Java 包装类与泛型复习笔记1. 学习目标掌握基本数据类型与包装类的关系。理解装箱、拆箱、自动装箱、自动拆箱。理解泛型的作用、语法和使用方式。掌握泛型类、泛型方法、泛型上界的基本写法。理解泛型擦除机制以及不能直接创建泛型数组的原因。2. 包装类2.1 为什么需要包装类Java 的基本数据类型不是Object的子类不能直接作为泛型类型实参使用。为了让基本数据类型也能参与面向对象和泛型相关的代码Java 为每个基本数据类型提供了对应的包装类。2.2 基本数据类型与包装类对应关系基本数据类型包装类byteByteshortShortintIntegerlongLongfloatFloatdoubleDoublecharCharacterbooleanBoolean记忆点除了Integer和Character其余包装类基本都是基本类型首字母大写。3. 装箱与拆箱3.1 装箱装箱是指把基本数据类型转换成对应的包装类对象。inti10;IntegeriiInteger.valueOf(i);// 推荐写法IntegerijnewInteger(i);// 旧写法不推荐3.2 拆箱拆箱是指把包装类对象中的值取出来转换成基本数据类型。IntegeriiInteger.valueOf(10);intjii.intValue();3.3 自动装箱与自动拆箱为了减少手动转换的代码量Java 提供自动装箱和自动拆箱。inti10;Integeriii;// 自动装箱相当于 Integer.valueOf(i)Integerij(Integer)i;// 自动装箱intjii;// 自动拆箱相当于 ii.intValue()intk(int)ii;// 自动拆箱自动装箱和自动拆箱只是编译器帮忙补充了转换代码本质上仍然会调用相关方法例如自动装箱Integer.valueOf(...)自动拆箱xxx.intValue()、xxx.doubleValue()等4. 面试题Integer 比较问题4.1 题目publicstaticvoidmain(String[]args){Integera127;Integerb127;Integerc128;Integerd128;System.out.println(ab);System.out.println(cd);}4.2 输出结果true false4.3 原因Integer a 127;会触发自动装箱本质上调用的是Integer.valueOf(127);Integer.valueOf对-128 ~ 127范围内的整数有缓存。这个范围内的值会复用已有对象所以ab比较的是同一个缓存对象的地址结果是true。而128超出了默认缓存范围通常会创建新的Integer对象所以cd比较的是两个不同对象的地址结果是false。重要结论比较引用类型时比较的是地址。包装类数值比较建议使用equals。不要依赖包装类缓存范围写业务逻辑。System.out.println(c.equals(d));// true5. 什么是泛型泛型是 JDK 1.5 引入的语法。通俗理解泛型就是让类、接口、方法可以适用于多种类型。从代码角度看泛型就是把数据类型参数化。例如MyArrayIntegerlistnewMyArray();MyArrayStringlist2newMyArray();同一个MyArray类可以通过传入不同的类型实参让它分别保存Integer、String等不同类型的数据。6. 为什么要引入泛型6.1 使用 Object 实现通用容器的问题如果想让一个数组能保存任意类型的数据可以使用Object[]。classMyArray{publicObject[]arraynewObject[10];publicObjectgetPos(intpos){returnthis.array[pos];}publicvoidsetVal(intpos,Objectval){this.array[pos]val;}}使用示例publicclassTestDemo{publicstaticvoidmain(String[]args){MyArraymyArraynewMyArray();myArray.setVal(0,10);myArray.setVal(1,hello);StringretmyArray.getPos(1);// 编译报错System.out.println(ret);}}问题Object[]可以保存任何类型类型约束太弱。取出元素时返回的是Object需要强制类型转换。编译器无法提前检查类型错误容易把问题留到运行时。正确写法需要强转Stringret(String)myArray.getPos(1);但强转存在风险如果取出的对象不是String运行时会发生ClassCastException。6.2 泛型解决的问题泛型的主要目的指定当前容器持有什么类型的对象让编译器在编译阶段进行类型检查。泛型的优势数据类型参数化。编译时自动类型检查。取数据时减少强制类型转换。代码复用性更好。类型错误更早暴露。7. 泛型类7.1 基本语法class泛型类名称类型形参列表{// 可以使用类型参数}示例classClassNameT1,T2,...,Tn{}泛型类也可以继承其他类并在继承关系中使用类型参数class泛型类名称类型形参列表extends继承类{// 可以使用类型参数}示例classClassNameT1,T2,...,TnextendsParentClassT1{// 可以只使用部分类型参数}7.2 常见类型形参命名规范类型形参常见含义EElement元素KKey键VValue值NNumber数字TType类型S、U、V第二、第三、第四个类型这些命名不是强制要求但遵守规范可以提高代码可读性。7.3 泛型类示例classMyArrayT{publicT[]array(T[])newObject[10];publicTgetPos(intpos){returnthis.array[pos];}publicvoidsetVal(intpos,Tval){this.array[pos]val;}}使用示例publicclassTestDemo{publicstaticvoidmain(String[]args){MyArrayIntegermyArraynewMyArray();myArray.setVal(0,10);myArray.setVal(1,12);intretmyArray.getPos(1);System.out.println(ret);myArray.setVal(2,bit);// 编译报错}}分析MyArrayInteger指定当前容器只能保存Integer类型。myArray.getPos(1)返回的是Integer可以自动拆箱为int。myArray.setVal(2, bit)试图放入String编译器会直接报错。8. 泛型类的使用8.1 基本语法泛型类类型实参变量名;// 定义泛型类引用new泛型类类型实参(构造方法实参);// 实例化泛型类对象示例MyArrayIntegerlistnewMyArrayInteger();8.2 类型推导当编译器可以从上下文推导出类型实参时实例化对象时可以省略右侧尖括号中的类型。MyArrayIntegerlistnewMyArray();这里右侧的称为钻石操作符编译器可以推导出它是Integer。8.3 泛型不能直接使用基本数据类型泛型只能接收类类型不能接收基本数据类型。错误写法MyArrayintlistnewMyArray();// 错误正确写法MyArrayIntegerlistnewMyArray();原因泛型需要使用引用类型基本数据类型要使用对应的包装类。9. 裸类型 Raw Type9.1 什么是裸类型裸类型是指使用泛型类时没有传入类型实参。MyArraylistnewMyArray();这里的MyArray就是裸类型。9.2 为什么不建议使用裸类型裸类型主要是为了兼容老版本 API 保留的机制不建议在新代码中主动使用。裸类型的问题失去泛型的类型检查能力。取数据时通常需要强制类型转换。更容易出现运行时类型转换异常。推荐写法MyArrayIntegerlistnewMyArray();10. 泛型的编译机制类型擦除10.1 什么是类型擦除Java 泛型主要在编译阶段发挥作用。编译器会进行类型检查生成字节码后运行期间通常不保留具体的泛型类型信息。在没有指定类型边界时类型参数T通常会被擦除为Object。例如classMyArrayT{publicT[]array;publicTgetPos(intpos){returnthis.array[pos];}publicvoidsetVal(intpos,Tval){this.array[pos]val;}}编译后T会被擦除字节码层面更接近于使用Object。通过javap -c查看字节码时可以看到字段或方法签名中出现java/lang/Object相关信息。10.2 类型擦除的核心结论泛型检查主要发生在编译期。运行期间通常没有完整的泛型类型信息。没有指定边界时类型参数通常擦除为Object。指定上界时类型参数会擦除为对应的上界类型。示例classMyArrayT{}T默认可以理解为TextendsObject如果写成classMyArrayEextendsNumber{}那么E会按照Number这个上界进行处理。11. 为什么不能直接创建泛型数组11.1 错误写法T[]tsnewT[5];// 错误原因泛型存在类型擦除运行时无法准确知道T到底代表什么具体类型因此不能直接创建T[]。11.2 常见但有风险的写法classMyArrayT{publicT[]array(T[])newObject[10];publicTgetPos(intpos){returnthis.array[pos];}publicvoidsetVal(intpos,Tval){this.array[pos]val;}publicT[]getArray(){returnarray;}}使用publicstaticvoidmain(String[]args){MyArrayIntegermyArray1newMyArray();Integer[]integersmyArray1.getArray();}可能出现异常Exception in thread main java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;原因array实际上是通过new Object[10]创建的。返回时试图把Object[]当作Integer[]使用。运行时数组类型不匹配所以可能发生ClassCastException。11.3 更安全的处理方式方式一返回Object[]。publicObject[]getArray(){returnarray;}方式二通过反射创建指定类型的数组。importjava.lang.reflect.Array;classMyArrayT{publicT[]array;publicMyArray(){}publicMyArray(ClassTclazz,intcapacity){array(T[])Array.newInstance(clazz,capacity);}publicTgetPos(intpos){returnthis.array[pos];}publicvoidsetVal(intpos,Tval){this.array[pos]val;}publicT[]getArray(){returnarray;}}使用publicstaticvoidmain(String[]args){MyArrayIntegermyArray1newMyArray(Integer.class,10);Integer[]integersmyArray1.getArray();}这种方式通过Integer.class明确告诉程序要创建什么类型的数组。12. 泛型上界12.1 为什么需要泛型上界有时候不能让泛型接收任意类型而是需要限制类型实参的范围。例如只希望某个泛型类接收Number或Number的子类。这时可以使用泛型上界。12.2 基本语法class泛型类名称类型形参extends类型边界{// ...}注意这里使用的是extends不仅可以表示继承某个类也可以表示实现某个接口。12.3 示例限制为 Number 的子类publicclassMyArrayEextendsNumber{// ...}使用MyArrayIntegerl1;// 正确Integer 是 Number 的子类MyArrayStringl2;// 编译错误String 不是 Number 的子类编译错误含义类型实参String不在类型变量E的边界范围内。12.4 默认上界如果没有显式指定类型边界classMyArrayE{}可以理解为classMyArrayEextendsObject{}12.5 示例限制必须实现 Comparable 接口publicclassMyArrayEextendsComparableE{// ...}含义E必须具备和同类型对象进行比较的能力。这种写法常见于需要比较大小、排序、查找最大值或最小值的场景。13. 泛型方法13.1 基本语法方法限定符类型形参列表返回值类型 方法名称(形参列表){// ...}示例publicstaticEvoidswap(E[]array,inti,intj){Etarray[i];array[i]array[j];array[j]t;}注意静态泛型方法需要在static后面使用E声明泛型类型参数。完整示例publicclassUtil{publicstaticEvoidswap(E[]array,inti,intj){Etarray[i];array[i]array[j];array[j]t;}}13.2 使用类型推导调用泛型方法Integer[]a{1,2,3,4,5};Util.swap(a,0,4);String[]b{a,b,c};Util.swap(b,0,2);编译器可以根据数组类型自动推导E的具体类型。13.3 不使用类型推导调用泛型方法Integer[]a{1,2,3,4,5};Util.Integerswap(a,0,4);String[]b{a,b,c};Util.Stringswap(b,0,2);这种写法显式指定了泛型方法的类型实参一般不常写除非编译器无法推导或为了强调类型。14. 易错点总结14.1 泛型不能使用基本数据类型错误MyArrayintlistnewMyArray();正确MyArrayIntegerlistnewMyArray();14.2 不能直接创建泛型数组错误T[]arraynewT[10];可替代T[]array(T[])newObject[10];// 有风险更安全array(T[])Array.newInstance(clazz,capacity);14.3 包装类比较不要随便使用不推荐Integera128;Integerb128;System.out.println(ab);// false推荐System.out.println(a.equals(b));// true14.4 不建议使用裸类型不推荐MyArraylistnewMyArray();推荐MyArrayIntegerlistnewMyArray();14.5 泛型只在编译期做主要检查泛型的类型安全主要由编译器保证。由于类型擦除运行期间通常无法直接获得完整泛型类型信息。15. 核心知识速记包装类用于让基本数据类型以对象形式参与泛型和面向对象编程。自动装箱本质是调用包装类的valueOf方法。自动拆箱本质是调用包装类的xxxValue方法。Integer在-128 ~ 127范围内通常会复用缓存对象。泛型的本质是数据类型参数化。泛型可以让编译器提前进行类型检查。泛型可以减少强制类型转换。泛型类型实参必须是引用类型不能是基本数据类型。裸类型是为了兼容旧版本代码保留的机制不建议主动使用。Java 泛型通过类型擦除实现。没有上界时类型参数默认上界是Object。有上界时类型参数会被限制在指定类型或其子类型范围内。T[] array new T[10]不合法因为运行时无法确定T的真实类型。泛型方法的类型参数写在返回值类型前面。