数据结构前置知识(java版)

初识泛型:从包装类到类型参数化

1 包装类:为什么需要它

Java 里基本类型(int、double 这些)不是继承自 Object 的,而泛型代码要求操作对象。为了在泛型里能支持基本类型,Java 给每个基本类型都配了一个包装类。

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

记法:除了 Integer 和 Character 这两个,其余包装类都是基本类型首字母大写。

1.1 手动装箱和拆箱

java 复制代码
int i = 10;
 
// 装箱:把基本类型的值"装"进对象里
Integer ii = Integer.valueOf(i); // 推荐
Integer ij = new Integer(i);     // 会额外创建对象,不推荐
 
// 拆箱:把对象里的值"取"出来
int j = ii.intValue();

1.2 自动装箱和自动拆箱

手动写 valueOf 和 intValue 太啰嗦,Java 干脆自动了:

java 复制代码
int i = 10;
Integer ii = i;          // 自动装箱
Integer ij = (Integer) i; // 自动装箱(强转写法)
int j = ii;              // 自动拆箱
int k = (int) ii;        // 自动拆箱(强转写法)

1.3 一道常考的面试题

java 复制代码
Integer a = 127;
Integer b = 127;
Integer c = 128;
Integer d = 128;
 
System.out.println(a == b); // true
System.out.println(c == d); // false

答案是第一个 true、第二个 false。原因是 Integer 内部有缓存:-128 到 127 之间的整数直接复用同一个对象,所以 127 那组 a 和 b 指向同一个对象,比较引用,相等;128 超出缓存范围,每次赋值都会新建对象,c 和 d 不是同一个对象 返回 false。

所以比较包装类相等,老老实实用 equals,别用 ==。这个坑写代码和面试都容易踩。

2 什么是泛型

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

泛型就是干这个的:JDK 1.5 引入的新语法,通俗讲就是适用于许多类型,从代码上讲,就是对类型实现了参数化。

3 引出泛型:Object 数组的坑

先看一个需求:实现一个类,里面有一个数组成员,希望它能存放任何类型的数据,还能按下标返回。

直觉做法:所有类的父类都是 Object,那把数组类型声明成 Object\[\] 不就行了?

java 复制代码
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"); // 字符串也能存进去
        String ret = myArray.getPos(1); // 编译报错
    }
}

问题很明显:

  • 任何类型都能往里放,但也意味着什么类型都可能取出来;
  • 取出来是 Object,想用回原来的类型必须强转;
  • 编译期完全不检查类型,写错了要等运行期才炸。
    现实里我们更希望:这个容器只持有一种类型,放错了编译器直接报错。于是泛型登场,把类型也当成参数传进去,需要什么类型就传什么类型。

3.1 泛型类的基本语法

java 复制代码
class 泛型类名称<类型形参列表> {
    // 类体里可以直接用类型参数
}
 
class ClassName<T1, T2, ..., Tn> {
    // 多个类型参数也行
}

类型形参的命名规范(了解即可,约定俗成):

  • E 表示 Element(元素)
  • K 表示 Key(键)
  • V 表示 Value(值)
  • N 表示 Number(数字)
  • T 表示 Type(类型)
  • S、U、V 等表示第二、第三、第四个类型

3.2 用泛型改写 MyArray

java 复制代码
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:不能直接 new T\[\] 泛型数组,这里先 new Object\[\] 再强转成 T\[\],属于过渡写法,不算好,后面擦除机制一节会解释原因;
  • 注释 2:使用时用 指定当前类型,new MyArray<>() 的尖括号里留空,因为编译器能推导出来;
  • 注释 3:取出来直接用 int 接,不需要强转;
  • 注释 4:存字符串直接编译报错,类被指定成 Integer 之后,编译器帮忙检查类型。

4 泛型类的使用

java 复制代码
MyArray<Integer> list = new MyArray<Integer>(); // 完整写法
MyArray<Integer> list = new MyArray<>();        // 类型推导,推荐

注意:泛型只能接受类类型,所有基本类型都必须用包装类,比如 MyArray 而不是 MyArray。

还有一个概念叫裸类型(Raw Type):泛型类不带类型实参,比如 MyArray list = new MyArray()。裸类型是为了兼容老版本 API 才保留的机制,自己写代码别用。

5 泛型怎么编译:擦除机制

泛型本质是编译期的东西。用 javap -c 反编译字节码看看,源码里的 T 全部变成了 Object:

text 复制代码
class MyArray<T> {
    public T[] array;
 
    MyArray();
    Code:
        0: aload_0
        5: bipush 10
        7: anewarray #2 // class java/lang/Object
        10: checkcast #3 // class "[Ljava/lang/Object;"
}

这种编译时把 T 替换成 Object 的机制,叫类型擦除。也就是说:Java 的泛型只在编译期起作用,编译器生成的字节码里不包含泛型类型信息,运行期一切都按 Object 处理。

5.1 那为什么不能直接 new T\[\]

现在可以回答前面的疑问了。T\[\] ts = new T5 不行,是因为擦除之后它相当于 Object\[\] ts = new Object5,而运行期把 Object\[\] 直接赋给 Integer\[\] 引用是不安全的:

java 复制代码
MyArray<Integer> myArray1 = new MyArray<>();
Integer[] arr = myArray1.getArray(); // 运行期抛 ClassCastException

擦除后 getArray 返回的是 Object\[\],里面可能装着任何类型,直接转成 Integer\[\] 编译器认为不安全,所以报错。

想真正创建指定类型的泛型数组,得借助反射(了解即可):

java 复制代码
class MyArray<T> {
    public T[] array;
 
    public MyArray(Class<T> clazz, int capacity) {
        array = (T[]) Array.newInstance(clazz, capacity);
    }
}
 
MyArray<Integer> myArray1 = new MyArray<>(Integer.class, 10);
Integer[] integers = myArray1.getArray();

6 泛型的上界

有时候不想让类型参数太自由,可以用 extends 给它加上界:

java 复制代码
class MyArray<E extends Number> {
    // E 只能是 Number 及其子类型
}
 
MyArray<Integer> l1; // 正常,Integer 是 Number 的子类
MyArray<String> l2;  // 编译错误,String 不是 Number 的子类

如果没有指定边界,默认等价于 extends Object。常见的复杂写法还有:

java 复制代码
// E 必须实现 Comparable 接口,说明 E 是可以比较大小的
class MyArray<E extends Comparable<E>> {
}

这种写法在排序、比较的场景里很常见,后面学 PriorityQueue、TreeMap 之类的源码时都会见到。

7 泛型方法

泛型不仅能用在类上,还能用在方法上。比如写一个交换数组两个位置元素的工具方法:

java 复制代码
public class Util {
    // 静态泛型方法:static 后面用 <E> 声明类型参数
    public static <E> void swap(E[] array, int i, int j) {
        E t = array[i];
        array[i] = array[j];
        array[j] = t;
    }
}

调用时可以让编译器推导类型:

java 复制代码
Integer[] a = {1, 2, 3};
swap(a, 0, 2);          // 自动推导 E 是 Integer
 
String[] b = {"a", "b"};
swap(b, 0, 1);          // 自动推导 E 是 String

也可以显式指定:

java 复制代码
Util.<Integer>swap(a, 0, 2);
Util.<String>swap(b, 0, 1);

小结

把这篇的内容串一遍:泛型就是把数据类型参数化,让代码能复用于多种类型;编译期它帮你做类型检查和自动转换,运行期又通过擦除机制把 T 还原成 Object,兼顾类型安全和性能。包装类是泛型能用于基本类型的前提,上界用来约束类型参数的范围,泛型方法让静态工具方法也能用上这套机制。

地基打完了,下一步就可以带着这些概念去拆 ArrayList、HashMap 的源码了。

相关推荐
步行cgn1 小时前
Spring 基于 XML 的自动装配:byType 详解
java·后端·spring
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day46|贯穿项目 2 思维导图复盘
java·开发语言·vue.js·学习·spring
智慧物业老杨3 小时前
物业日常巡查的数智化重构:从“打卡式巡检“到“闭环式风控“
android·java·人工智能·系统架构·rxjava
步行cgn10 小时前
Spring c 命名空间注入详解
java·后端·spring
明月_清风10 小时前
Maven 到底是什么?一篇文章搞懂 Java 项目构建与依赖管理
java·后端·maven
aramae11 小时前
MySQL复合查询(8)
java·c语言·开发语言·后端·算法
Rain的Java大神之路11 小时前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog12 小时前
Java 接入Redis: 通用命令与键管理
java·服务器·redis
Wang's Blog12 小时前
Java 接入Redis: 列表集合与有序集合操作命令
java·服务器·redis