本节目标
- 学习一个新的 java 语法泛型的使用
- 学习一个新的 java 概念,包装类
- List / ArrayList / LinkedList 的基本使用
- ArrayList 类的使用
- LinkedList 类的使用
1. 预备知识-泛型(Generic)
1.1 泛型的引入
问题:我们之前实现过的顺序表,只能保存 int 类型的元素,如果现在需要保存 指向 Person 类型对象的引用的顺序表,请问应该如何解决?如果又需要保存指向 Book 对象类型的引用呢?
回答:
- 首先,我们在学习多态过程中已知一个前提,基类的引用可以指向子类的对象。
- 其次,我们也已知
Object
是 java 中所有类的祖先类。
那么,要解决上述问题,我们很自然的想到一个解决办法,将我们的顺序表的元素类型定义成 Object
类型,这样我们的 Object
类型的引用可以指向 Person
类型的对象或者指向 Book
类型的对象了。
示例代码:
class MyArrayList {
private int[] elem;
private int usedSize;
public MyArrayList() {
this.elem = new int[10];
//this.elem = new E[10];
}
public void add(int val) {
this.elem[usedSize] = val;
usedSize++;
}
public int get(int pos) {
return this.elem[pos];
}
}
变为
class MyArrayList {
private Object[] elem;
private int usedSize;
public MyArrayList() {
this.elem = new Object[10];
//this.elem = new E[10];
}
public void add(Object val) {
this.elem[usedSize] = val;
usedSize++;
}
public Object get(int pos) {
return this.elem[pos];
}
}
这样,我们可以就可以很自由的存储指向任意类型对象的引用到我们的顺序表了。
示例代码:
public static void main(String[] args) {
MyArrayList myArrayList = new MyArrayList();
myArrayList.add(1);
myArrayList.add("hello");
//遗留问题:现在的 MyArrayList 虽然可以做到添加任意类型的引用到其中了
//但遇到以下代码就会产生问题。
int ret = (int)myArrayList.get(1);
// 将 Object 类型转换为 int 类型,需要类型转换才能成功
// 这里编译正确,但运行时会抛出异常 ClassCastException
System.out.println(ret);
}
//结果为:
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at TestDemo2.main(TestDemo2.java:93)
Process finished with exit code 1
提示:问题暴露的越早,影响越小。编译期间的问题只会让开发者感觉到,运行期间的错误会让所有的软件使用者承受错误风险。
所以我们需要泛型这种机制,可以
1. 增加编译期间的类型检查
2. 自动进行强制类型转换,不需要手动做类型转换
1.2 泛型的分类
1. 泛型类
2. 泛型方法
1.3 泛型类的定义的简单演示
关于泛型类的定义,这里只是了解即可,我们重点学习泛型类的使用。
// 1. 尖括号 <> 是泛型的标志
// 2. E 是类型变量(Type Variable),变量名一般要大写
// 3. E 在定义时是形参,代表的意思是 MyArrayList 最终传入的类型,但现在还不知道
public class MyArrayList<E> {
private E[] array;
private int size;
...
}
注意: 泛型类可以一次有多个类型变量,用逗号分割。
1.4 泛型类的使用
// 定义了一个元素是 Book 引用的 MyArrayList
MyArrayList<Book> books = new MyArrayList<Book>();
books.add(new Book());
//泛型已经自动进行强制类型转换,不需要手动做类型转换
Book book = book.get(0);
//此处会产生编译错误,Person 类型无法转换为 Book 类型
//实现了编译期间的类型检查
books.add(new Person());
//此处会产生编译错误,book.get(0)类型的返回值为 BOOK,无法转换为 Person 类型
//实现了编译期间的类型检查
Person person = book.get(0);
通过以上代码,我们可以看到泛型类的一个使用方式:只需要在所有类型后边跟尖括号,并且尖括号内是真正的类型,即 E 可以看作的最后的类型。
注意: Book 只能想象成 E 的类型,但实际上 E 的类型还是 Object。
泛型实现顺序表 示例:
class MyArrayList<E> {
private E[] elem;
private int usedSize;
public MyArrayList() {
this.elem = (E[])new Object[10];
//这一行虽然可以使用,但是也是错误的,因为实际上还是Object[]数组,
//此种强转成泛型的方法有可能出错,例如下面的main5()方法,
//Object[] 类整个强转成为String[] 类,就会报异常ClassCastException。
//要想真正创建数据的存放,需要用到反射。
//this.elem = new E[10]; //这一行是错误的,会出现编译错误
}
public void add(E val) {
this.elem[usedSize] = val;
usedSize++;
}
public E get(int pos) {
return this.elem[pos];
}
// public <T> T[] getArray(int size) {
// T[] genericArray = new T[size]; // suppose this is allowed
// return genericArray;
// }
public Object[] getArray(int size) {
Object[] genericArray = new Object[size];
return genericArray;
}
}
public class TestDemo2 {
public static void main5(String[] args) {
MyArrayList<String> myArrayList1 = new MyArrayList<>();
String[] rets = (String[])myArrayList1.getArray(10);
}
//getArray()方法返回Object[]类,Object[]类整个强转成为String[]类,
//就会报异常ClassCastException。
public static void main4(String[] args) {
MyArrayList<String> myArrayList1 = new MyArrayList<>();
System.out.println(myArrayList1);
MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
System.out.println(myArrayList2);
MyArrayList<Boolean> myArrayList3 = new MyArrayList<>();
System.out.println(myArrayList3);
}
//func4()结果为:
MyArrayList@1b6d3586
MyArrayList@4554617c
MyArrayList@74a14482
public static void main3(String[] args) {
MyArrayList<String> myArrayList1 = new MyArrayList<>();
myArrayList1.add("ABC");
myArrayList1.add("bit");
String ret = myArrayList1.get(1);
System.out.println(ret);
MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
myArrayList2.add(1);
myArrayList2.add(12);
myArrayList2.add(31);
int ret2 = myArrayList2.get(1);
System.out.println(ret2);
}
public static void main2(String[] args) {
MyArrayList<String> myArrayList1 = new MyArrayList<>();
MyArrayList<Integer> myArrayList2 = new MyArrayList<>();
MyArrayList<Boolean> myArrayList3 = new MyArrayList<>();
}
}
注意:
- 泛型不能实例化对象,如下图
//this.elem = new E[10]; //这一行是错误的,会出现编译错误
- 泛型的语法上必须匹配。使用子类或超类,也必须使用通配符来匹配,比如这里要方法参数需要定义为
List<? extends Object>
即可接受List<String>
的对象 - 子类之间不能互相引用或者继承(下图为特例)
Object[] o2 = new coffee[10]; //特例,平级关系也可以引用。
- func()4的代码没有打印<>中的包装类的原因是:泛型当中尖括号<>中的内容,不参与类型的组成。
Object[] 强制类型转换的思考:对于main5()方法的解释
因此我们总结一下,运行时异常ClassCastException出现试图将一个对象强制转换为一个并非是自己或自己的子类对象的时候
至此回到我们文章开头的问题,为什么我将Object,强制转为Coffee[]就会抛错呢?
抛错分析
实际上,在java中,Object是所有class类型的根,数组当然也是一种class类型,因此Object[]类型和Coffee[]类型都是Object类型的子类型
但是对于Object[]类型和Coffee[]类型,二者同为数组类型,可他们之间并没有什么父子关系,而是平级的关系,所以无法强制类型转换
正是因为很多同学误以为Object[]应该是Coffee[]的父类型,才会随手写出导致程序运行时ClassCastException异常
二者区别
Object[]类型与Coffee[]类型的区别在于其数组中可以存放任意Object类型的对象,而Coffee[]则只能存放所有的Coffee类型(包括其子类)对象
总结
因此,对于日常开发中,我们应该重视对Object[] 类型的返回值的处理,不能想当然的认为他就是Xxx[],随手进行强制类型转换,而导致出现运行时类型转换异常。
不建议对Object[]数组进行强制类型转换。
1.父类引用引用父类对象,父类引用无法进行子类的强制类型转换为子类。
2.父类引用引用子类对象,父类引用可以进行子类的强制类型转换为子类。
3.子类转换成父类,父类引用只能操作其非特有的属性(由父类继承而来的属性)。
具体查看:https://blog.csdn.net/qq_44309916/article/details/111042647
具体查看:Java - 基本数据类型 - 强制类型转换部分
来源链接:https://www.jianshu.com/p/9783e29d7c20
MyArrayList<String> myArrayList1 = new MyArrayList<>();
//左边尖括号写了类型,右边尖括号就可以不写,
数组和泛型之间的一个重要区别是它们如何强制执行类型检查。
具体来说,数组在运行时存储和检查类型信息。然而,泛型在编译时检查类型错误,并且在运行时没有类型信息。
泛型创建数组最正确的方式:通过反射进行创建。
面试问题:泛型是怎么编译的?
泛型是编译时期的一种机制,擦除机制。
例如把上面的代码中String、Integer、Boolean类全部擦除,变成Object类。
1. 泛型是作用在编译期间的一种机制,即运行期间没有泛型的概念。
2. 泛型代码在运行期间,就是我们上面提到的,利用 Object 达到的效果(这里不是很准确,以后会做说明)。
1.5 泛型总结
- 泛型是为了解决某些容器、算法等代码的通用性而引入,并且能在编译期间自动对类型进行检查。
- 泛型可以自动对类型进行强制类型转换。
- 泛型利用的是 Object 是所有类的祖先类,并且父类的引用可以指向子类对象。
- 泛型是一种编译期间的机制,即 MyArrayList 和 MyArrayList 在运行期间是一个类型。
- 泛型是 java 中的一种合法语法,标志就是尖括号 <>
2. 预备知识-包装类(Wrapper Class)
Object 引用可以指向任意类型的对象,但有例外出现了,8 种基本数据类型不是对象,那岂不是刚才的泛型机制要失效了?
实际上也确实如此,为了解决这个问题,java 引入了一类特殊的类,即这 8 种基本数据类型的包装类,在使用过程中,会将类似 int 这样的值包装到一个对象中去。
2.1 基本数据类型和包装类直接的对应关系
基本就是类型的首字母大写,除了 Integer 和 Character。
2.2 包装类的使用,装箱(boxing)和拆箱(unboxing)
Integer a = 123;//装箱 装包【隐式的】
int b = a;//拆箱 拆包【隐式的】
Integer a2 = Integer.valueOf(123);//显示的装包
Integer a3 = new Integer(123);//显示的装包
int b2 = a2.intValue();//显示的拆包
double d = a2.doubleValue();//显示的拆包
int i = 10;//显示的初始化
可以看到在使用过程中,装箱和拆箱带来不少的代码量,所以为了减少开发者的负担,java 提供了自动机制。
int i = 10;
Integer ii = i; // 自动装箱
Integer ij = (Integer)i; // 自动装箱