Java泛型和内部类

Posted 爱敲代码的三毛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java泛型和内部类相关的知识,希望对你有一定的参考价值。


一、泛型的概述

1.概念

我们都用过Java集合类,比如 ArrayList、 LinkedList 、HashMap等,都可以用来存放数据,但如果不使用泛型那么就会变成一个通用的集合类。
来看一个列子:

public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(1);
        list.add(150.50);
        list.add("泛型");
        System.out.println(list);
}    

运行结果

我们发现这个集合可以存任意类型的数据,相当于一个裸类型,显然这不是我们希望的。我们希望的是存一组相同类型的数据。那么就需要用到泛型了。

2.泛型的定义

注意:泛型的参数一定是一个引用类型

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
} 
class ClassName<T1, T2, ..., Tn> {
}

【规范】类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element(集合中使用的数组)
  • K 表示 Key(键)
  • V 表示 Value(值)
  • N 表示 Number(数值类型)
  • T 表示 Type(Java类)
  • S, U, V 等等 - 第二、第三、第四个类型

3.泛型的使用

泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
ArrayList<Integer> arrayList1 = new ArrayList<>();//可以推导出实例化需要的类型实参为 Integer

同样使用ArrayList,利用泛型为它指定能存放的数据类型后。它就只可以存放指定的类型了,一旦想存放其它类型的元素在编译期间就会报错。

我们自己定义了一个栈指定了存放元素类型为 Interger,那么它就只能存Integer的元素

class Stack<T> {
    public T[] objects;
    public int top;

    public Stack() {
        this.objects = (T[])new Object[10];
    }

    public void push(T obj) {
        objects[this.top++] = obj;
    }

    public T get() {
        return objects[this.top-1];
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        stack.push(10);
        stack.push(20);
        System.out.println(stack);
    }
}

4.泛型的意义

其实就是将类型参数化了

1.存放数据的时候,进行类型的检查

2.取数据的时候,进行类型的自动转换

5.泛型是如何编译的?

泛型是在编译期间的一种机制,这种机制叫做:类型擦除机制。
编译的时候,会把这个泛型参数,擦为 Object

注意:是类中的泛型被擦成Object了,其它地方的泛型都是被擦没了。
比如下面的泛型参数都是在编译期间都被擦没了。

而类里的泛型参数就变被擦成了 Object,比如这里的 T

这里要说到泛型的一个坑:就是泛型的数组是不能 new 的,因为泛型是先检查后编译的,当检查的时候并不指定你的泛型参数是什么。我们可以看一下ArrayList的源码,它传的是一个 E类型的参数,实质上 new 的是一个Object类型的数组。所以要记住泛型的数组是不能 new 的!


二、泛型的进一步使用

1.泛型类的定义-类型边界

假如我们要写一个泛型类来求一种元素中的最大值,那么就会遇到一个很尴尬的问题。那就是引用类型不能直接用大于小于号比较。

我们知道引用类型比较大小可以使用 ComparbleComparator,前面说过类中的泛型参数 T会被擦成 Object、但Object中并没有实现这两个接口,就无法对引用类型进行比较。

当被擦到Object后,没有实现对应的接口,所以我们就要控制不要擦除到Object,指定泛型的擦除边界。

语法:使用extends 关键字设置擦除边界

Algorithm<T>//擦除到Object
Algorithm<T extends Comparable<T>>//擦除到Comparable

代码示例:

class Algorithm<T extends Comparable<T>>{
    public T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                T tmp = max;
                max = array[i];
                array[i] = tmp;
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Algorithm<Integer> algorithm = new Algorithm<>();
        Integer[] array = {1,2,3,100,80,156,20,25};
        int ret = algorithm.findMax(array);
        System.out.println("最大的数为:"+ret);
    }
}

运行结果:

下面这个代码代表着泛型的上界

注意:泛型只有上界没有下界

下面这个代码的意思就是:擦除到了Comparable这个接口的地方,换句话说就是将来这个T类型一定是实现了这个接口。

Number类

如果以后看见一下写法,那么 E 一定是实现了 Number 的子类

public class MyArrayList<E extends Number> {
}

Number 的子类有下面这几个

2.泛型方法

上面通过的泛型类来求最大值的时候,每次都要 new 对象非常的麻烦。为了不那么麻烦,就可以用到泛型方法了。
我们给泛型方法直接添加了一个 static并指定了擦除边界,就可以直接通过类名来调用了。

class Algorithm{
    public static<T extends Comparable> T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                T tmp = max;
                max = array[i];
                array[i] = tmp;
            }
        }
        return max;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Integer[] array = {1,2,3,100,80,156,20,25};
        int ret = Algorithm.findMax(array);
        System.out.println("最大的数为:"+ret);
    }
}

注意:这里没有指定泛型的参数,但它会根据 形参的类型 推导出 整个泛型的类型参数

当然你也可以自己指定参数,不过一般不这么写

int ret1 = Algorithm.findMax(array);
int ret2 = Algorithm.<Integer>findMax(array);//一般会省略这个

3.泛型中的父子类型

注意一个问题:

<> 里面的内容,不构成类型的组成

MyArrayList 不是 MyArrayList 的父类型
MyArrayList 也不是 MyArrayList 的父类型

4.通配符 ?

我们来写一个泛型方法和通配符方法来打印集合里的元素

运行结果

这两种方法好像没有什么太大区别,来看一下它们的add和get方法。
我们发现,通配符的add方法在添加元素的时候并不知道添加的是什么元素。
而获取元素,而get方法则是只需要个下标就可以了

也就是说通配符只适合读数据,所以通配符一般只是存在于源码当中。

通配符上界

<? extends 上界>

用法和泛型类似

代码示例:

// 可以传入类型实参是 Number 子类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? extends Number> list) {
...
} 
// 以下调用都是正确的
printAll(new MyArrayList<Integer>());
printAll(new MyArrayList<Double>());
printAll(new MyArrayList<Number>());
// 以下调用是编译错误的
printAll(new MyArrayList<String>());
printAll(new MyArrayList<Object>())

通配符下界

语法:

<? super 下界>

传入的类型是下界的夫类或者是下界的本身

代码示例:

// 可以传入类型实参是 Integer 父类的任意类型的 MyArrayList
public static void printAll(MyArrayList<? super Integer> list) {
...
} 
// 以下调用都是正确的
printAll(new MyArrayList<Integer>());
printAll(new MyArrayList<Number>());
printAll(new MyArrayList<Object>());
// 以下调用是编译错误的
printAll(new MyArrayList<String>());
printAll(new MyArrayList<Double>());

5.泛型的限制

  1. 泛型类型参数不支持基本数据类型
  2. 无法实例化泛型类型的对象
  3. 无法使用泛型类型声明静态的属性
  4. 无法使用 instanceof 判断带类型参数的泛型类型
  5. 无法创建泛型类数组
  6. 无法 create、catch、throw 一个泛型类异常(异常不支持泛型)
  7. 泛型类型不是形参一部分,无法重载

三、内部类

1.本地内部类

方法里面定义的类:无任何意义

2.实例内部类

1.实例内部类中不能定义静态的成员变量

因为静态的成员变量不依赖于对象

如果非要定义可以定义为 static final修饰的常量,也就是说只要编译期间能确定的值就可以

2.如何在其它类拿到一个实例内部类的对象

通过 外部类.内部类 变量名 = 外部类对象的引用.new 内部类

class ATest {
    public int a = 10;
    class InnerClass {
        public int a = 100;
        public static final int c = 20;
    }
}
public class TestDemo2 {
    public static void main(String[] args) {
        ATest aTest = new ATest();
        ATest.InnerClass innerClass = aTest.new InnerClass();
    }
}

有的书上是简写

ATest.InnerClass innerClass1 = new ATest().new InnerClass();

3.实例内部类当中如何访问和外部类相同的成员变量

外部类名.this.变量名

class ATest {
    public int a = 10;
    class InnerClass {
        public int a = 100;
        public static final int c = 20;
        public void fuc() {
            System.out.println(ATest.this.a);
        }
    }
}
public class TestDemo2 {
    public static void main(String[] args) {
        ATest aTest = new ATest();
        ATest.InnerClass innerClass = aTest.new InnerClass();
        innerClass.fuc();
    }
}

运行结果

3.静态内部类

1.如何拿到静态内部类的对象

直接通过 外部类.静态类名就好了

class ATest {
    public int a = 10;
    static class InnerClass {
        public int a = 100;
        public static final int c = 20;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        ATest.InnerClass innerClass = new ATest.InnerClass();
    }
}

2.静态内部类中,不能访问外部类的非静态的数据成员。

如果非要在静态内部类中访问非静态的成员变量,也是能实现的。
在内部类中提供一个外部类的成员变量,给静态内部类当中提供一个带一个参数的构造方法,参数是外部类对象,通过这个成员变量来访问.

4.匿名内部类

当你的类只想定义一次且没有名字时,就可以使用匿名内部类。

接口使用

public class TestDemo {
    interface A {
        public void func();
    }

    A a = new A(){
        @Override
        public void func() {
            System.out.println("当前是个匿名内部类,实现了A接口,重写了接口的方法");
        }
    };
    public static void main(String[] args) {
   
    }
}

匿名子类

class ATest {
    public void func() {
        System.out.println("func()");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        new ATest(){
            @Override
            public void func() {
                System.out.println("重写的func方法");
            }
        }.func();
    }
}

运行结果:


注意事项:
匿名内部类中:一定是程序在运行的过程当中没有发生改变的量

如果不修改tmp就可以运行,有的书上会说匿名内部中只能是常量。其实是在运行过程中没有发生改变的量


完!

以上是关于Java泛型和内部类的主要内容,如果未能解决你的问题,请参考以下文章

Java 基础语法详解 Java 中的泛型

Java 基础语法详解 Java 中的泛型

java里的泛型和通配符

Java 基础语法Java 的泛型和包装类

Java进阶泛型和多线程

java泛型和通配符