泛型的使用与内部类的刨析

x33g5p2x  于2022-02-07 转载在 其他  
字(9.8k)|赞(0)|评价(0)|浏览(361)

一、泛型(generic)的定义

泛型是能够将一个类型转变为多个类型使用。例如一个单链表中不用泛型则我们实现的时候只能放入一种类型的数据,但是用泛型后能够将多种类型的数据都能放入。

1.泛型的语法

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

class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, …, Tn> extends ParentClass<T 1> {
// 可以只使用部分类型参数
}

尖括号当中也可以定义多个类型实参。意义是能够指定多种类型的存储。

类型形参一般使用一个大写字母表示,常用的名称有:
E 表示 Element
K 表示 Key
V 表示 Value
N 表示 Number
T 表示 Type
S, U, V 等等 - 第二、第三、第四个类型

2.泛型的简单使用示例

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];
    }
}

二、泛型类的使用

1.含有泛型类的泛型对象的创建

泛型类<类型实参> 变量名; // 定义一个泛型类引用
new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

示例:MyArrayList<String> list = new MyArrayList<String>();

不过后面尖括号当中的类型实参可以省略,但是必须要加上;省略里面的类型实参编译器会自动推导。如:MyArrayList<String> list = new MyArrayList<>(); // 可以推导出实例化需要的类型实参为 String

2.裸类型(Raw Type)

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型。如:MyArrayList list = new MyArrayList();

3.泛型的类型边界

在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。

语法:

class 泛型类名称<类型形参 extends 类型边界> { ... 
}

示例:

public class MyArrayList<E extends Number> {
  ...
}

extends后面的称为泛型的上界,即传入的实参类型只能是泛型上界它的子类或者泛型上界它本身。

在上面示例的基础上,在创建泛型类的对象时,只能创建Number类的子类或者它本身。如:

MyArrayList<Integer> l1; // 正常,因为 Integer 是 Number 的子类型 MyArrayList<String> l2; // 编译错误,因为 String 不是 Number 的子类型

了解: 没有指定类型边界 E,可以视为 E extends Object 。

泛型是没有下界的,只有上界;而通配符是存在上界和下界的,下面会讲到。

4.泛型的类型擦除机制

**泛型是在编译时期的一种机制–擦除机制。编译的时候,会因为擦除机制泛型参数全部被擦除为Object类型,因此它能够将泛型里面的引用类型参数化了。**因此,我们在一个泛型类当中定义一个泛型数组时,创建的泛型数组实例化要用Object[]的数组强制转换为泛型类型。如:T[] objects = (T[])new Object[10];

注:我们不能直接new 泛型类型的数组,如T[] t = new T[];,因为泛型是先检查后编译的,检查的时候不知这个T是哪个类型,而编译的时候直接将其擦除为Object。因此有T[] t = (T[])new Object[]

泛型的<>里面的内容,不构成类型的组成。
如:

public class Test {
    public static void main(String[] args) {
        Algorithm<Integer> algorithm = new Algorithm<>();
        System.out.println(algorithm);
    }
}

打印结果:Algorithm后面<>的参数类型不见了,可见它不构成类型的组成

5.泛型E extend 接口使用实例

如:定义一个泛型类搜索树。

public class BSTree<K extends Comparable<K>> { 
   ... 
}

传入的K必须是实现了Comparable接口的引用类型,并且Comparable里面的K是比较的类型。

它的意思是:将泛型传入的参数擦除到实现Comparable接口的类型当中。又因为Object类中没有实现Comparable接口,因此我们要实现引用的比较时要定义上界的类有实现Comparable接口。

6.泛型的意义

泛型的意义:

  • 存数据的时候,会自动进行类型的检查。
  • 取数据的时候,进行类型的自动转换。

例如:
Stack当中的objects数组的类型为Object[],里面能够放入所有类型的数据,若取出某一个数据时,需要强制类型转换。

class Stack {
    public Object[] objects;
    public int top;

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

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

    public Object get() {
        return objects[this.top-1];
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push(1);
        stack.push(2);
        stack.push("zjr");
        String str = (String)stack.get();
    }

而泛型能够完美地解决这一问题。它能够指定一个数组当中放入的是什么类型(可以指定一种类型,也可以指定多种类型)的数据,因此它能够自动帮我们做类型的检查。当我们取出某个数据时,如上面例子中的String str = (String)stack.get();,在泛型下不需要强制类型转换,直接String str =stack.get();即可。

三、泛型类的使用

1.通配符

用于在泛型的使用,即为通配符。

T有相同的地方也有不同的地方。
相同:都能够指定任意类型的数据。
不同:能够自己定义上界与下界,而T只能定义上界。

public class MyArrayList<E> {...} 
// 可以传入任意类型的 MyArrayList 
public static void printAll(MyArrayList<?> list) { 
   ... 
}
// 以下调用都是正确的 
printAll(new MyArrayList<String>()); 
printAll(new MyArrayList<Integer>()); 
printAll(new MyArrayList<Double>()); 
printAll(new MyArrayList<Number>()); 
printAll(new MyArrayList<Object>());

2.普通泛型与通配符的打印方式

ArrayList<T>,则遍历的时候每个元素都先默认为T类型,并且在方法中的static后加上< T >去识别T是泛型。

ArrayList<?>,每个元素的类型不是?类型,因此不能直接把T改成?。因为编译期有擦除机制将类型擦为Object类型,因此可以先将每个元素当成Object类型,并且Object类型是所有类型的父类。

class Test {

    public static<T> void print(ArrayList<T> list) {

        for (T t : list) {
            System.out.println(t);
        }
    }

    //代表通配符  擦除机制  Object
    public static void print2(ArrayList<?> list) {
        for (Object t : list) {
            System.out.println(t);
        }
    }
}

public class TestDemo2 {

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Test.print(list);
        System.out.println("===============");
        Test.print2(list);

    }

2.通配符的上界

语法:<? 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>());

3.通配符的下界

语法:<? 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>());

4.泛型中的父子类型

public class MyArrayList<E> { ... } 
// MyArrayList<Object> 不是 MyArrayList<Number> 的父类型 
// MyArrayList<Number> 也不是 MyArrayList<Integer> 的父类型 
// 需要使用通配符来确定父子类型 
// MyArrayList<?> 是 MyArrayList<? extends Number> 的父类型 
// MyArrayList<? extends Number> 是 MyArrayList<Integer> 的父类型

5.通配符与普通类型的区别

首先我们在泛型方法中传入ArrayList<T>,调用ArrayList中的add方法。发现add方法中能够放入的是我们指定的类型。

而在泛型方法中传入ArrayList<?> list,调用ArrayList中的add方法,发现放入的不知道是什么类型。

所以通配符一般存放的地方大多是在源码当中。

若都调用get方法,则获取的都是整型数据。

因此在ArrayList< T >下适合写东西,在ArrayList<?>下适合从某个东西里面读。

四、泛型方法

1.泛型方法的设置

例子:当我们要求一个数组当中的最大的数。

(1) 用泛型类实现:在类的后面extends Comparable接口说明上界是实现Comparable接口的类型。在编译的擦除机制中将T类型擦除为实现Comparable接口的类型。

因为在泛型中的参数是引用类型,不能直接比较max<array[i],要用到Comparable比较器。

如:

class Algorithm<T extends Comparable<T>> {
    public T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            //max < array[i]
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

(2)用泛型方法实现
普通类当中实现泛型方法,在返回值T前加上<T extends Comparable<T>>,说明T在擦除机制处理后擦除为实现Comparable接口的类型,因此m有max.compareTo(array[i])

class Algorithm2 {
    //泛型方法
    public static<T extends Comparable<T>> T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

2.调用泛型方法

泛型方法会根据形参的类型推导出整个泛型的类型参数,因此跟普通方法的调用方式是一致的。

class Algorithm2 {
    //泛型方法
    public static<T extends Comparable<T>> T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}
    public static void main(String[] args) {
        Integer[] integers = {1,2,13,4,5};
        //会根据 形参的类型 推导出 整个泛型的类型参数
        Integer ret = Algorithm2.findMax(integers);
        System.out.println(ret);
        //一般这个就省略了
        Integer ret2 = Algorithm2.<Integer>findMax(integers);
        System.out.println(ret2);
    }

五、泛型的限制

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

六、内部类

1.实例内部类

特点:
1.在实例内部类中不能定义静态的成员变量。原因:静态的成员变量不依赖于对象,而实例内部类是依赖于对象去获得的。如果非要定义,可以定义为static final常量类型,因此在编译期间能确定的值就可以。
2.实例内部类的实例化,需要运用到外部类的对象。左边拿类型,右边拿对象。

class Test {
    public static void main1(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        OuterClass.InnerClass innerClass1 = new OuterClass().new InnerClass();
    }
}

3.若要调用实例内部类中的方法,则直接用InnerClass类型的名字调用实例类中方法的名字即可。

class OuterClass {
    public void funcInner() {
        System.out.println("OuterClass::func()");
    }

    /**
     * 实例内部类:
     * 可以把实例内部类  当做就是一个实例成员
     */
    class InnerClass {
        public void funcInner() {
            System.out.println("InnerClass::funcInner");
        }
    }
}
class Test {
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.funcInner();
    }
}

4.实例内部类中访问外部类不同名的成员变量可以直接访问,而同名的成员变量需要用到外部类对象。

class OuterClass {
    public int data1 = 1;
    private int data2 = 2;
    public static int data3 = 3;

    public void func() {
        System.out.println("OuterClass::func()");
    }
    
    class InnerClass {
        public int data1 = 1000;
        public int data4 = 4;
        private int data5 = 5;
        //public static int data6 = 6;
        public static final int data6 = 6;

        public void funcInner() {
            System.out.println("InnerClass::funcInner");
            System.out.println(this.data1);//1000
            System.out.println(data2);//2
            System.out.println(data3);//3
            System.out.println(OuterClass.this.data1);//1
        }
    }
}
class Test {
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.funcInner();
    }
}

可见,this关键字可以看作OuterClass中的一个静态变量,否则无法用类名去调用this。可以这样去理解。

5.内部类的字节码文件表现形式:
一个类对应一个字节码文件,但是内部类的字节码文件是由:外部类$内部类.class组成。

2.静态内部类

特点:
1.静态内部类里面可以定义静态属性,并且什么类型的属性都可以定义。
2.静态内部类访问属性是依赖于类的,而外部类是访问属性要依赖于对象。因此在静态内部类中是无法访问外部类的属性的。若非要访问,则可以:

class OuterClass {
    public int data1 = 1;
    public int data2 = 2;
    public static int data3 = 3;

    static class InnerClass {
        public OuterClass out;//内部类当中设置外部类的对象

        public InnerClass(OuterClass out) {
            this.out = out;
        }

        public void func1() {
            System.out.println(data1);//编译无法通过
            System.out.println(out.data1);//1
        }
    }
}
class Test {
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = new OuterClass.InnerClass(outerClass);
        innerClass.func1();
    }
}

注意:InnerClass里面的构造方法就是为了实例化public OuterClass out; 因此在main方法当中要传入一个已经实例化的outerClass引用。

3.匿名内部类

1.**匿名内部类的使用是在实例化new 对象时后面直接加花括号。**花括号当中可以重写实例化对象的类当中的方法。该方法可以自由使用。若要调用匿名内部类中重写的方法,则直接在匿名内部类花括号的后面调用即可。

class OuterClass {
    public void func() {
        System.out.println("OuterClass::func()");
    }
}
class Test {
    public static void main(String[] args) {
        new OuterClass(){
            @Override
            public void func() {
                System.out.println("haha");
            }
        }.func();
    }
}
//打印结果:haha

2.匿名内部类可以在接口中使用。如:

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

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

有一个匿名内部类在接口中使用,说明定义了一个类是实现了该接口的,并且重写接口中的方法。

3.**在匿名内部类当中,访问的数据一定是在运行的过程中没有发生改变的量,这样的现象称为变量捕获。**如:

class OuterClass {
    public void func() {
        System.out.println(111);
    }
}
class Test {
    public static void main(String[] args) {
        int local = 199;

        //匿名子类
        new OuterClass(){
            @Override
            public void func() {
                local = 777;//编译出错
                System.out.println("我是重写的func()");
                System.out.println(local);
            }
        }.func();
    }
}

七、内部类的使用场景

之前,我们都是这样定义的:以树为例,结点的类与树的类是分开写的。

class Node {
    public int data;
    public Node left;
    public Node right;
}
class Tree {
    
}

但是学过内部类后,我们可以将结点的类定义到树当中,说明树是由一个个结点组成的。可以将结点的类定义成一个内部类。

class Tree {
    public static class Node {
        public int data;
        public Node left;
        public Node right;
    }
}

相关文章