• 【Java】泛型的理解与使用,包装类


    在这里插入图片描述博客主页: XIN-XIANG荣
    系列专栏:【Java SE】
    一句短话: 难在坚持,贵在坚持,成在坚持!

    一. 引出泛型

    一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的 代码,这种刻板的限制对代码的束缚就会很大。----- 来源《Java编程思想》对泛型的介绍。

    泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现参数化。

    我们知道所有类的父类,默认为Object类,那么我们可以实现数组的类型为Object;此时数组中可以存放任意类型的数据,看下面给出的代码:

    class MyArray {
        public Object[] array = new Object[10];
        public Object getPos(int pos) {
            return this.array[pos];
        }
        public void setVal(int pos,Object val) {
            this.array[pos] = val;
        }
    }
    public class TestDemo {
        public static void main(String[] args) {
            MyArray myArray = new MyArray();
            //存放整形
            myArray.setVal(0,10);
            //字符串也可以存放
            myArray.setVal(1,"hello");
            //也可以存放char,float,自定义类型……
            //编译报错,这里是向下转型,需要进行强制类型转换
            String ret = myArray.getPos(1);
            System.out.println(ret);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    上面的代码存在两个问题,可以想象如果在实际开发中写出这样的代码,对于代码的维护是非常不友好的;

    1. 任何类型数据都可以存放
    2. 1号下标本身就是字符串,但是确编译报错;必须进行强制类型转换

    虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种或几种数据类型;而不是同时持有这么多类型。

    所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象,让编译器去做检查;此时,就需要把类型,作为参数传递;需要什么类型,就传入什么类型。

    二. 泛型类

    1. 泛型类的语法

    【语法】

    类名后的代表占位符,表示当前类是一个泛型类,T是形参,用来接收传入的数据类型(实参)。

    class 泛型类名称<类型形参列表> {
    // 这里可以使用类型参数
    }
    
    class ClassName<T1, T2, ..., Tn> {
    }
    class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
    // 这里可以使用类型参数
    }
    
    class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
    // 可以只使用部分类型参数
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    • E 表示 Element
    • K 表示 Key
    • V 表示 Value
    • N 表示 Number
    • T 表示 Type
    • S, U, V 等等 - 第二、第三、第四个类型

    使用泛型就可以解决1中的问题,将1中的代码改写如下:

    class MyArray<T> {
        //不能实例化泛型类的数组
        public T[] array = (T[])new Object[10];//1
        public T getPos(int pos) {
            return this.array[pos];
        }
        public void setVal(int pos,T val) {
            this.array[pos] = val;
        }
    }
    
    public class TestDemo {
        public static void main(String[] args) {
            //类型参数化,指定要放入的数据的类型
            MyArray<Integer> myArray = new MyArray<>();//2
            myArray.setVal(0,10);
            myArray.setVal(1,12);
            //不需要自己去实现强转
            int ret = myArray.getPos(1);//3
            System.out.println(ret);
            //编译错误,会进行类型检查
            //myArray.setVal(2,"bit");//4
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    【注意事项】:

    1. 注释1处,不能new泛型类型的数组,下面给出的写法是错误的
    T[] ts = new T[5];//是不对的
    
    • 1
    1. 注释2处,类型后加入指定当前类型(实参),要注意的是这里的类型不能是基本数据类型,只能是类类型(具体的一个类)

    2. 注释3处,不需要进行强制类型转换,泛型会自动进行强转。

    3. 注释4处,代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释4处,编译器会在存放元素的时候帮助我们进行类型检查,当存放的元素与指定的类型不符时就会编译报错。

    2. 泛型类的使用

    【语法】

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

    【代码示例】

    MyArray<Integer> list = new MyArray<Integer>();
    
    • 1

    注意:泛型只能接受类,所有的基本数据类型必须使用包装类!

    【类型推导】(Type Inference)

    当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写

    MyArray<Integer> list = new MyArray<>(); 
    // 可以推导出实例化需要的类型实参为 String
    
    • 1
    • 2

    【小结】

    1. 泛型是将数据类型参数化,进行传递
    2. 使用表示当前类是一个泛型类。
    3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换

    3. 裸类型(Raw Type)

    裸类型是一个泛型类但没有带着类型实参,下面给出的就是一个裸类型

    class MyArray<T> {
        //……
    }
    //裸类型
    MyArray list = new MyArray();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    裸类型并没有传递类型给形参,此时编译器也就不会自动进行类型检查和强制类型转换

    所以我们不要自己去使用裸类型,裸类型是为了兼容老版本的 API 保留的机制

    三. 泛型是如何编译的

    1. 擦除机制

    在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制

    Java的泛型机制是在编译期间实现的,编译器生成的字节码在运行期间并不包含泛型的类型信息。

    img

    img

    2. 不能实例化泛型类数组的原因

    上面已经提到过,泛型类数组是不能直接进行实例化的,去new一个泛型类的数组编译是通不过的

    T[] ts = new T[5];
    
    • 1

    img

    因为对于Java的数组来说,在编译时必须知道它持有的所有对象的具体类型,也就是说要实例化T类型的数组,编译器在编译时需要获得T类型;但我们知道泛型是具有擦除机制的,T类型在编译时会被擦除掉,此时也就不存在所谓的泛型了,自然也就无法进行实例化了;所以编译会报错。

    我们可以通过反射去实现泛型的实例化:

    class MyArray<T> {
        public T[] array;
        public MyArray() {
        }
        
        /**
         * 通过反射创建,指定类型的数组
         * @param clazz 指定类型
         * @param capacity 指定容量
         */
        public MyArray(Class<T> clazz, int capacity) {
            array = (T[]) Array.newInstance(clazz, capacity);
        }
        
        public T getPos(int pos) {
            return this.array[pos];
        }
        public void setVal(int pos,T val) {
            this.array[pos] = val;
        }
        public T[] getArray() {
            return array;
        }
    
        public static void main(String[] args) {
            MyArray<Integer> myArray1 = new MyArray<>(Integer.class,10);
            Integer[] integers = myArray1.getArray();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    五. 泛型方法

    【语法】

    方法限定符 <类型形参列表> 返回值类型 方法名称(形参列表){
        //...
    }
    
    • 1
    • 2
    • 3

    【定义示例】

    public class Util {
        //静态的泛型方法 需要在static后用<>声明泛型类型参数
        public static <E> void swap(E[] array, int i, int j) {
            E t = array[i];
            array[i] = array[j];
            array[j] = t;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    【使用示例】

    不使用类型推导

    Integer[] a = { ... };
    Util.<Integer>swap(a, 0, 9);
    
    String[] b = { ... };
    Util.<String>swap(b, 0, 9);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用类型推导

    Integer[] a = { ... };
    swap(a, 0, 9);
    
    String[] b = { ... };
    swap(b, 0, 9);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    六. 泛型的上界

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

    【语法】

    class 泛型类名称<类型形参 extends 类型边界> {
       //...
    }
    
    • 1
    • 2
    • 3

    泛型的的上界可以是类,也可以是接口;

    上界为类,那么实参必须为上界的子类或者上界本身

    //只接受 Number 的子类型作为 E 的类型实参
    public class MyArray<E extends Number> {
        //...
    }
    
    MyArray<Integer> l1; // 正常,因为 Integer 是 Number 的子类型
    MyArray<String> l2; // 编译错误,因为 String 不是 Number 的子类型
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上界为接口,那么实参必须是实现了该接口的类

    //E必须是实现了Comparable接口的
    public class MyArray<E extends Comparable<E>> {
        //...
    }
    
    • 1
    • 2
    • 3
    • 4

    【使用实例】

    创建两个Person对象,并以其中的age属性进行比较

    Person实现了Comparable接口,设置比较方法所在类的泛型上界为Comparable,可以将比较方法设置为实例方法或者是静态方法

    class Person implements Comparable<Person>{
    
        public int age;
    
        public Person(int age) {
            this.age = age;
        }
    
        @Override
        public int compareTo(Person o) {
            return this.age-o.age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    '}';
        }
    }
    
    class Alg1<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 ) {
                    max = array[i];
                }
            }
            return max;
        }
    }
    class Alg2 {//类名处不设置泛型
        //实例泛型方法,在方法处设置泛型和上界
        public <T extends Comparable<T>> T findMax (T[] array) {
            T max = array[0];
            for (int i = 1; i < array.length; i++) {
                //if(max < array[i]) {
                if(max.compareTo(array[i]) < 0 ) {
                    max = array[i];
                }
            }
            return max;
        }
    }
    class Alg3{
        //静态泛型方法
        //不依赖对象,必须在方法处设置上界
        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 class Test {
        public static void main(String[] args) {
            Person[] people = {new Person(10),new Person(15)};
    
            //Alg1 alg = new Alg1<>();
            //Person person = alg.findMax(people);
    
            Alg2 alg = new Alg2();
            Person person = alg.findMax(people);
    
            //Person person = Alg3.findMax(people);
    
            System.out.println(person);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    注意观察上面的代码,上面的代码给泛型设置了上界Comparable,此时可以将两个Person对象进行比较,但如果没有设置上界Comparable,泛型在编译时就会被擦除为Object,而Object是没有实现Comparable接口的,无法进行比较功能;所以这里设置了上界,擦除时会擦除为Comparable类型

    img

    七. 通配符

    1. 介绍

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

    通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student 是 Person 的子类,那么 List< Student > 也应 该是 List< Person >的子类。但是泛型是不支持这样的父子类关系的。

    泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围.

    观察下面的代码fun方法,

    class Message<T> {
        private T message ;
        public T getMessage() {
            return message;
        }
        public void setMessage(T message) {
            this.message = message;
        }
    }
    
    public class TestDemo1 {
        public static void main(String[] args) {
            Message<Integer> message = new Message() ;
            message.setMessage(99);
            fun(message); // 出现错误,只能接收String
        }
        public static void fun(Message<String> temp){
            System.out.println(temp.getMessage());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    解决上述问题只需要修改fun方法<>中的参数为通配符即可;要注意这里使用通配符后可以获取元素;但不可以修改元素,因为传入的类型是未知的,无法确定要放入的是什么类型的元素。

    // 此时使用通配符"?"描述的是它可以接收任意类型,
    //但是由于不确定类型,所以去无法修改数组当中的内容
    public static void fun(Message<?> temp){
        //temp.setMessage(100); 无法修改!
        System.out.println(temp.getMessage());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2. 通配符上界

    ? extends 类:设置泛型上限

    【语法】

    <? extends 上界>
    <? extends Number>//可以传入的实参类型是Number或者Number的子类
    
    • 1
    • 2

    通配符的上界,不能进行写入数据,只能进行读取数据。

    img代码示例:

    class Food {
    }
    class Fruit extends Food {
        //...
    }
    class Apple extends Fruit {
        //...
    }
    class Banana extends Fruit {
        //...
    }
    class Plate<T> {
        private T plate ;
    
        public T getPlate() {
            return plate;
        }
    
        public void setPlate(T plate) {
            this.plate = plate;
        }
    }
    public class TestDemo {
        public static void main(String[] args) {
            Plate<Fruit> plate1 = new Plate<>();
            plate1.setPlate(new Fruit());
            fun(plate1);
    
            Plate<Food> plate2 = new Plate<>();
            plate2.setPlate(new Food());
            
            //编译报错,fun上界为Fruit,不能接收Fruit的父类
            //fun(plate2);
        }
        
        public static void fun(Plate<? extends Fruit> temp) {
            //编译报错,不能修改
            //temp.setPlate(new Apple());
            
            //可以访问
            Fruit fruit = temp.getPlate();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    观察上面的代码,无法在fun方法中对temp进行修改元素,因为temp接收的是Fruit和他的子类,此时存储的元素应该是哪个子类无法确定;所以修改会报错!但是可以获取元素。

    3. 通配符下界

    ? super 类:设置泛型下限

    【语法】

    <? super 下界>
    <? super Integer>//代表 可以传入的实参的类型是Integer或者Integer的父类类型
    
    • 1
    • 2

    通配符的下界,不能进行读取数据,只能写入数据。

    img 代码示例:

    public class TestDemo {
        public static void main(String[] args) {
            Plate<Apple> plate1 = new Plate<>();
            plate1.setPlate(new Apple());
            //fun(plate1);//编译报错
    
            Plate<Food> plate2 = new Plate<>();
            plate2.setPlate(new Food());
            fun(plate2);
        }
        public static void fun(Plate<? super Fruit> temp){
            //可以存放Fruit本身或者其子类
            temp.setPlate(new Apple());
            temp.setPlate(new Banana());
    
            //编译报错,不能存放Fruit的父类
            temp.setPlate(new Food());
            
            //不能取数据,因为无法知道取出的父类的类型是什么?
            //Fruit fruit = temp.getPlate();//编译报错  
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    观察上面的代码,fun方法中不能获取元素,因为通配符接收的是Fruit的父类,无法获知获取到的是哪个父类型的元素;但是可以存放元素(向上转型是没有问题的)。

    八. 包装类

    1. 介绍

    在Java中,由于基本类型不是继承自Object,也就是说基本数据类型是不具有对象特征的,它们不能像对象一样拥有属性和方法,以及对象化交互。

    为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了 一个包装类型;通过包装类可以让基本数据类型获取和对象一样的特征,行使对象相关的权力。

    除了 Integer 和 Character, 其余基本类型的包装类都是首字母大写

    基本数据类型包装类
    byteByte
    shortShort
    intInteger
    longLong
    floatFloat
    doubleDouble
    charCharacter
    booleanBoolean

    【作用】

    • 作为和基本数据类型对应的类类型存在,方便涉及到对象的操作
    • 提供每种基本数据类型的相关属性如最大值、最小值等以及相关的操作方法

    2. 与基本数据类型的转化

    装箱:

    • 把基本数据类型转换成包装类,分为自动装箱和手动装箱

    拆箱:

    • 把包装类转换成基本数据类型,分为自动拆箱和手动拆箱

    【代码示例】

    //装箱:把基本数据类型转换成包装类
    //1、自动装箱
    int i = 10;
    Integer ii = i;
    Integer ij = (Integer)i;
    //2、手动装箱:使用构造方法
    //新建一个 Integer 类型对象,将 i 的值放入对象的某个属性中
    int i = 10;
    Integer ii = Integer.valueOf(i);
    Integer ij = new Integer(i);
    
    //拆箱:把包装类转换成基本数据类型
    //1、自动拆箱
    int j = ii; 
    int k = (int)ii;
    //2、手动拆箱:使用intValue方法
    int j = ii.intValue();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3. 常用方法和常量

    intValue():包装类转为int类型
    valueOf():int类型转包装类
    parseInt():字符串转int类型
    toString():包装类转为字符串
    与其他进制的转换:

    toHexString():十六进制

    toOctalString():八进制

    toBinaryString():二进制

    MAX_VALUE : 表示int型可取的最大值
    MIN_VALUE : 表示int型可取的最小值

    4. 基本类型与包装类型的异同

    1. 在Java中,一切皆对象,但八大基本类型对象。
    2. 声明方式不同,基本类型无需通过new关键字来创建,而包装类型需new关键字。
    3. 存储方式及位置不同,基本类型是直接存储变量的值保存在栈中能高效的存取,包装类型需要通过引用指向实例,具体的实例保存在堆中。
    4. 初始值不同,包装类型的初始值为null,基本类型比如int类型的初始值为0
    5. 使用方式不同,比如泛型中基本数据类型只能使用包装类

    5. 一道面试题

    下列代码输出什么,为什么?

    public static void main(String[] args) {
            Integer a = 127;
            Integer b = 127;
            Integer c = 128;
            Integer d = 128;
            System.out.println(a == b);
            System.out.println(c == d);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    执行结果:

    img

    分析:

    代码中是将int类型的数据转化为Intgetr类型的数据(装箱),这里虽然是自动装箱,但在底层一定是调用了valueOf()实现的装箱;那么这里我们就观察一些Integer类中 alueOf() 方法的源码

    img

    img

    可以看到在源码中当数剧的值在 [low, high]([-128,127] 源码中可以看到low和high的值为-128和127) 范围内时是直接返回cache数组中的元素的,题目中的 a和b 对应值都为127,在范围内,所以拿到的是数组中的同一个元素,引用自然是相同的;

    而c和d对应值为128,不在范围内,源码中可以看到此时是新new了一个Intgetr对象返回;两个不同的对象,引用自当是不等的。

  • 相关阅读:
    2023衡阳师范学院计算机考研信息汇总
    利用maven的dependency插件将项目依赖从maven仓库中拷贝到一个指定的位置
    如何用一部手机输出视频内容
    大话设计模式解读02-策略模式
    React - 高级用法
    Linux日志管理
    9.30作业
    谷歌数据中心尝试转向主线内核,发起新的项目Project Icebreaker
    leetcode 654 最大二叉树
    Go-Excelize API源码阅读(十六)——GetPageLayout、SetPageMargins
  • 原文地址:https://blog.csdn.net/Trong_/article/details/126808011