DataStructure.包装类&简单认识泛型

【本节目标】

  1. 以能阅读 java 集合源码为目标学习泛型
  2. 了解包装类
  3. 了解泛型

1 包装类

在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。

1.1 基本数据类型和对应的包装类

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

除了 IntegerCharacter, 其余基本类型的包装类都是首字母大写。

1.2 装箱和拆箱

public class Test {
    public static void main(String[] args) {
        int i = 10;

        //装箱操作:新建一个Integer类型对象,将i的值放进对象的某个属性中
        Integer ii = Integer.valueOf(i);
        Integer ij = new Integer(i);

        //拆箱操作:将Integer对象中的值取出,放到一个基本数据类型中
        int j = ii.intValue();
    }
}

1.3 自动装箱和自动拆箱

可以看到在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制。

public class Test {
//可以看到在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制。
    public static void main2(String[] args) {
        int a = 10;

        Integer b = a; // 自动装箱
        Integer c = (Integer) a; // 自动装箱

        int j = b; // 自动拆箱
        int k = (Integer) b; // 自动拆箱 
    }
}

在这里插入图片描述
【面试题】

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

public class Test {
    //下列代码输出是什么,为什么?
    public static void main(String[] args) {
        Integer a = 127;
        Integer b = 127;

        Integer c = 128;
        Integer d = 128;

        System.out.println(a == b);//true
        System.out.println(c == d);//false
    }
}

2 什么是泛型

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

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

3 引出泛型

实现一个类,类中包含一个数组成员,使得数组中可以存放任何类型的数据,也可以根据成员方法返回数组中某个下标的值?

思路:

  1. 以前学过的数组,只能存放指定类型的元素,例如:int[] array = new int[10]; String[] array = new String[10];
  2. 所有类的父类,默认为Object类,数组是否创建为Object

代码示例:

class MyArray {
   public Object[] objects = new Object[10];
   //输入
   public Object getObjects(int pos) {
       return objects[pos];
   }
   //输出
   public void setObjects(int pos, Object obj) {
       this.objects[pos] = obj;
   }
}

public class Test {
    public static void main(String[] args) {
        MyArray myArray = new MyArray();
        myArray.setObjects(0, 100);
        myArray.setObjects(1, "hello");//字符串也可以存放

        String ret = (String)myArray.getObjects(1);
        //String ret = myArray.getObjects(1);//编译报错
        System.out.println(ret);

    }
}

问题:以上代码实现后 发现

  1. 任何类型数据都可以存放
  2. 下标1本身是字符串,但是编译报错,必须进行强制类型转换
  • 虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望他只能够持有一种数据类型。而不是同时持有这么多类型。
  • 所以,泛型的主要目的:就是指定当前的容器,要持有什么样的类型。让编译器去做检验。 此时,就需要把类型,作为参数传递,需要什么的类型,就传入什么类型。

3.1 语法

class 泛型类名称<类型形参列表> {
    //这里可以使用类型参数
}

class ClassName<T1, T2, T3...> {
}

class 泛型类型名称<类型参数列表> extends 继承类/*这里可以使用类型参数*/{
    //这里可以使用类型参数
}

class ClassName<T1, T2, T3...>extends ParentClass<T1>{
    //可以使用部分类型参数
}

上述代码进行改写如下:

class MyArray1 <T> {
    public T[] objects = (T[])new Object[10];//1
    public T getObjects(int pos) {
        return objects[pos];
    }
    public void setObjects(int pos, T obj) {
        this.objects[pos] = obj;
    }
}

public class Test1 {
    public static void main(String[] args) {
        MyArray1<Integer> myArray11 = new MyArray1<>();//2
        myArray11.setObjects(1, 100);
        int ret1 = myArray11.getObjects(1);//3
        System.out.println(ret1);

        System.out.println(myArray11.getObjects(1));

        //myArray11.setObjects(2,"bit");//4
    }
}

代码解释:

  1. 类名后的代表占位符,表示当前类是一个泛型类
    了解: 【规范】类型形参一般使用一个大写字母表示,常用的名称有:
  • E表示Element
  • K表示Key
  • V表示Value
  • N表示Number
  • T表示Type
  • S, U, V 等等 - 第二、第三、第四个类型
  1. 注释1处,不能new泛型类型的数组
    意味着:
T[] ts = new T[];//error

课件当中的代码:T[] array = (T[])new Object[10];是否就足够好,答案是未必的。这块问题一会儿介绍。

  1. 注释2处,类型后加入<Integer>指定当前类型
  2. 注释3处,不需要进行强制类型转换
  3. 注释4处,代码编译报错,此时因为在注释2处指定类当前的类型,此时在注释4处,编译器会在存放元素的时候帮助我们进行校验检查。

4 泛型类的使用

4.1 语法

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

4.2 示例

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

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

4.3 类型推导(Type Inference)

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

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

5. 裸类型(Raw Type) (了解)

5.1 说明

裸类型是一个泛型类但没有带着类型实参,例如 MyArrayList 就是一个裸类型

MyArray List = new MyArray();

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

下面的类型擦除部分,我们也会讲到编译器是如何使用裸类型的。

小结:

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

6 泛型如何编译的

6.1 擦除机制

那么,泛型到底是怎么编译的?这个问题,也是曾经的一个面试问题。泛型本质是一个非常难的语法,要理解好他还是需要一定的时间打磨。

通过命令:javap -c 查看字节码文件,所有的T都是Object

在这里插入图片描述
在编译的过程当中,将所有的T替换为Object这种机制,我们称为:擦除机制

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

提出问题:

1、那为什么,T[] ts = new T[5]; 是不对的,编译的时候,替换为Object,不是相当于:Object[] ts = new Object[5]吗?
2、类型擦除,一定是把T变成Object吗?

6.2 为什么不能实例化泛型类型数组

代码1:

class MyArray2 <T> {
    public T[] array = (T[]) new Object[10];

    public T getPos(int pos) {
        return array[pos];
    }

    public void setVal(int pos, T val) {
        this.array[pos] = val;
    }
    public T[] getArray() {
        return array;
    }
}

public class Test {
    public static void main(String[] args) {
        MyArray2<Integer> myArray2 = new MyArray2<>();
        Integer[] string = myArray2.getArray();
    }
}

/*
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
 */

原因:替换后的方法为:将Object[]分配给Integer[]引用,程序报错.

public Object[] getArray() {
	return array;
}

通俗讲就是:返回的Object数组里面,可能存放的是任何的数据类型,可能是String,可能是Person,运行的时候,直接转给Integer类型的数组,编译器认为是不安全的。

正确的方式:【了解即可】

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 class Test1 {
    public static void main(String[] args) {
        MyArray<Integer> myArray1 = new MyArray<>(Integer.class, 10);
        Integer[] integers = myArray1.getArray();
    }
}

7 泛型的上界

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

7.1语法

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

7.2 示例

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

只接受Number的子类型作为E的类型实参

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

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

7.3 复杂示例

写一个泛型类,当中的方法,可以求数组中的最大值

//写一个泛型类,当中的方法,可以求数组中的最大值
class Alg<E extends Comparable<E>> {
    public E findMax(E[] array) {
        E max = array[0];
        for (int i = 0; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

public class TestGeneric {
    public static void main(String[] args) {
        Integer[] array = {1, 2, 3, 4, 5, };
        Alg<Integer> alg = new Alg<>();
        Integer ret = alg.findMax(array);
        System.out.println(ret);
    }
}

代码包含两个类:AlgTestGeneric。其中,Alg 是一个泛型类,用于找到数组中的最大值,而 TestGeneric 是包含 main 方法的测试类,用于演示如何使用 Alg 类来找到整数数组中的最大值。

Alg 类

  1. 泛型定义Alg 类被定义为一个泛型类 Alg<E extends Comparable<E>>,这意味着它接受一个类型参数 E,这个类型必须实现 Comparable<E> 接口。这样,Alg 类就能对 E 类型的对象进行排序和比较。

  2. findMax 方法

    • 这个方法接受一个 E 类型的数组作为参数。
    • 它首先将数组的第一个元素赋值给变量 max,作为初始的最大值。
    • 然后,它遍历整个数组,使用 compareTo 方法比较当前最大值 max 和数组中的每个元素。
    • 如果当前元素大于 max(即 max.compareTo(array[i]) < 0),则更新 max 为当前元素。
    • 最后,返回找到的最大值。

TestGeneric 类

  1. main 方法
    • 首先,定义了一个 Integer 类型的数组 array,并初始化了一些值。
    • 然后,创建了一个 Alg<Integer> 的实例 alg
    • 调用 algfindMax 方法,将整数数组作为参数传入,并将返回的最大值存储在变量 ret 中。
    • 最后,打印出找到的最大值。

逻辑关系和总结

  • Alg 类利用了 Java 的泛型机制,使其能够处理实现了 Comparable 接口的任何类型的数据。这提供了很大的灵活性,因为你可以用它来找到任何可比较类型数组中的最大值。
  • TestGeneric 类中,通过创建一个 Alg<Integer> 的实例并调用其 findMax 方法,我们演示了如何使用这个泛型类来找到整数数组中的最大值。
  • compareTo 方法用于比较两个对象的大小,这是 Comparable 接口的一部分。在这个例子中,它用于确定数组中的最大值。
  • 最终,程序打印出数组 {1, 2, 3, 4, 5} 中的最大值,即 5

整个程序的逻辑关系是:定义一个能找数组中最大元素的泛型类 -> 实例化这个类并传入特定类型的数组 -> 找出并打印数组中的最大值。

8 泛型方法

8.1 定义语法

方法限定符<类型参数列表> 返回值类型 方法名称(相残列表) {
    ...
}

8.2 示例

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

8.3 使用示例

//写一个泛型类,当中的方法,可以求数组中的最大值
class Alg2 {
    public <E extends Comparable<E>> E findMax(E[] array) {
        E max = array[0];
        for (int i = 0; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}

public class TestGeneric {
	public static void main2(String[] args) {
	    Integer[] array = {1, 2, 3, 4, 5, };
	    Alg2 alg2 = new Alg2();
	    Integer ret = alg2.findMax(array);
	    //Integer ret = alg2.<Integer>findMax(array);
	    System.out.println(ret);
	}
}

在这段代码中,Alg2 类不再是一个泛型类,而是其中的 findMax 方法是一个泛型方法。这意味着这个方法可以接受任何实现了 Comparable 接口的类型数组,并返回该类型数组中的最大值。

Alg2 类

  • findMax 方法被声明为泛型方法,通过 <E extends Comparable<E>> 来定义类型参数 E,这表明 E 必须实现 Comparable<E> 接口。
  • 方法的实现逻辑与之前相同:初始化 max 为数组的第一个元素,遍历数组,通过 compareTo 方法比较每个元素,并更新 max 如果找到更大的元素,最后返回 max

TestGeneric 类

  • main 方法中,创建了一个 Alg2 的实例 alg2
  • 调用 alg2findMax 方法,并传入一个 Integer 类型的数组 array。由于 Integer 实现了 Comparable<Integer>,因此可以作为 findMax 方法的参数。
  • 将返回的最大值存储在变量 ret 中,并打印出来。

逻辑关系和总结

  1. 泛型方法findMax 是一个泛型方法,它允许在方法调用时确定类型参数,而不是在类实例化时。这提供了灵活性,因为你可以对不同类型的数组调用同一个方法,只要这些类型实现了 Comparable 接口。

  2. 类型推断:在调用 findMax 方法时,Java 编译器能够根据传入的数组类型自动推断出类型参数 E。因此,在调用 alg2.findMax(array) 时,编译器知道 E 应该是 Integer 类型。这就是为什么你可以直接写 Integer ret = alg2.findMax(array); 而不是显式指定类型参数(如 alg2.<Integer>findMax(array))。

  3. 代码复用:通过使用泛型方法,Alg2 类可以很容易地用于找到任何实现了 Comparable 接口的类型数组中的最大值,从而提高了代码的复用性。

  4. 输出:程序将打印出数组 {1, 2, 3, 4, 5} 中的最大值,即 5

总的来说,这段代码展示了如何使用泛型方法来编写灵活且可重用的代码,以便处理不同类型的数组并找到其中的最大值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值