- 0. 泛型的本质
- 0. 泛型的目的
- 1. 泛型的语法
- 2. 包装类
- 3 .泛型如何编译
- 4.泛型的上界
- 5. 通配符
- 有坑填坑
0. 泛型的本质
泛型的本质:泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多类型。从代码上讲,就是对类型实现了参数
化。
总的来说,泛型就是把类型参数化了,即给类型一个指定的参数,在使用时指定参数具体的值,这个类型就可以在使用的时候决定了,这样就可以编写应用于多种类型的代码。
泛型代码注释&#xff1a;List<> 给String类型&#xff0c;代表List这个集合操作的是String类型的数据&#xff0c;且&#xff0c;&#61; 后面的ArrayList<> &#xff0c;这里的<> 里的值可以省略。 |
---|
|
0. 泛型的目的
泛型的目的&#xff1a;是指定当前容器&#xff0c;要持有什么类型的对象&#xff0c;让编译器去做检查&#xff0c;检查类型的安全性&#xff0c;并且所有的强制转换都是隐式转换的。此时&#xff0c;就需要把类型&#xff0c;作为参数传递&#xff0c;提高了代码的复用性。
作用 |
---|
1.安全性 2.消除转换 3. 提高性能 4. 复用性 |
1. 泛型的语法
class 泛型类的名称<类型形参列表>{
}
class Student<T1,T2....Tn>{
}
class 泛型类型名称<类型形参列表> extends 继承类<类型形参列表>
class Mike<Integer> extends Student<Integer>{
}
class Rose<T1,T2..Tn> extends Student<T1>{
}
1.1 泛型的使用
泛型类 | public class Student {...;} |
---|
泛型方法 | public T func(T[] arr,T root){....;} |
泛型接口 | public calss Student implements Comparable{...;} |
- 泛型类代码实例&#xff1a;
T 代表占位符&#xff0c;表示这个类或者是变量、方法是一个泛型&#xff0c;需要传类型参数&#xff0c;就好比一个函数有一个参数&#xff0c;你调用它需要给它传参&#xff0c;否则就会报错&#xff0c;这个T 其实就是形参名&#xff0c;可以改成E、K...V、T 都可以 |
---|
了解&#xff1a;【命名规范】类型形参一般使用一个大写字母表示&#xff0c;常用的名称有&#xff1a;
E | 表示Element(元素) |
---|
K | 表示Key(钥匙、键) |
V | 表示Value(值) |
N | 表示Number(数) |
T | 表示Type(类型) |
S,U,V等 | 第二&#xff0c;第三&#xff0c;第四个类型 |
- 注释1处&#xff1a;不能直接new泛型的数组
我是先new一个Object数组&#xff0c;然后强制转换为T
类型
|
---|
T[] arr &#61; new T[10]//error |
- 注释2处&#xff1a;类型(Student)后加入
是指定当前类型,new Student<>
&#xff0c;这里的<>内可加也可不加Integer
。 - 注释3处&#xff1a;不需要强制类型转换&#xff0c;Integer是包装类&#xff0c;可以自动拆箱和装箱。这也就提高了效率。
- 注释4处&#xff1a;代码编译报错&#xff0c;因为在注释2处指定类的当前类型是
Integer
&#xff0c;此时在注释4处&#xff0c;存放元素时&#xff0c;编译器就会帮助我们进行类型检查。
2. 包装类
在JAVA中&#xff0c;由于基本类型不是继承于Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;JAVA给每个基本类型都对应了一个包装类。
基本数据类型 | 包装类 |
---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
2.1 装箱和拆箱
int a &#61; 10;
Integer b &#61; Integer.value(a);
Integer c new Integer(a);
int d &#61; c.intValue();
在下面几行行代码&#xff0c;编译器自动调用了Integer.valueof()
&#xff0c;属于自动装包和拆包。
2.2.1练习题
- 【挖坑&#xff1a;】下列代码输出什么&#xff0c;为什么&#xff1f;
public static void main(String[] args) {
Integer a &#61; 100;
Integer b &#61; 100;
Integer c &#61; 200;
Integer d &#61; 200;
System.out.println(a &#61;&#61; b);
System.out.println(c &#61;&#61; d);
}
3 .泛型如何编译
在编译的过程中&#xff0c;将所有的T
都替换为Object的这种机制&#xff0c;称为&#xff1a;擦除机制。JAVA的泛型机制是在编译时实现的&#xff0c;编译器生成的字节码在运行期间并不包含泛型的类型信息(运行时没有泛型这么一说了)。
- Why&#xff1f;
T[] T &#61; new T[10];
是不对的。编译的时候替换为Object&#xff0c;不是相当于&#xff1a;Object[] T &#61; new Object[10];
- 类型擦除&#xff1a;一定把T擦除给Object吗&#xff1f;
T[]
擦除成了Object&#xff0c;Object是不能用其他类型接收的&#xff0c;因为Object可以存各种类型&#xff0c;把Object里的元素取出来转换成某个具体元素&#xff0c;是不安全的&#xff0c;JAVA不支持这么做。
数组和泛型之间有一个重要的区别就是&#xff1a;它们是如何强制执行类型检查。 数组在运行时存储和检查类型信息&#xff0c;而泛型是在编译时检查类型错误。
所以&#xff1a;泛型是不能实例化泛型类型数组。前面我写的T[] t &#61; (T[]) new Obeject[10];
其实也是错的。
解答2. |
---|
一定是都擦除成了Object&#xff0c;前面我已经说过了。 |
4.泛型的上界
class 泛型类名称 <类型形参> extends<类型形参> {
}
-
示例&#xff1a;
class Myarray{
}
class Int extends Myarray{
}
class Long extends Int{
}
public class Maths<E extends Myarray>{
}
解释&#xff1a;Maths
的E
只能是Myarray
的子类&#xff0c;也就是说**E
接受的类型实参只能是Myarray
的子类**。
-
再看一个例子&#xff1a;
class Student<E extends Number>{
E[] arr;
E Name;
}
public class Test {
public static void main(String[] args) {
Student<Integer> in;
Student<String> str;
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6YVc6XOJ-1665319877790)(C:\Users\COFFEEWEN\AppData\Roaming\Typora\typora-user-images\image-20221009194623454.png)]
**解释&#xff1a;**注释1处&#xff0c;编译正确&#xff0c;因为Integer
是Number
的子类型&#xff0c;注释2处&#xff0c;编译错误&#xff0c;因为String
不是Number
的子类型。
5. 通配符
通配符&#xff1a; |
---|
? 用于在泛型的使用&#xff0c;即为通配符 |
> 无边界通配符 |
extends E> 上界的通配符&#xff0c;E为上界 |
super E> 下界的通配符&#xff0c;E为下界 |
通配符是用于解决泛型之间引用传递问题的特殊语法&#xff0c;更为灵活&#xff0c;多用于扩大参数的范围。
class Fruits<T>{
private T fruits;
public T getFruits(){
return this.fruits;
}
public void setFruits(T fruits){
this.fruits &#61; fruits;
}
}
public class Demo {
public static void main(String[] args) {
Fruits<String> fruits1 &#61; new Fruits<>();
fruits1.setFruits("苹果");
put(fruits1);
Fruits<Integer> fruits2 &#61; new Fruits<>();
fruits2.setFruits(100);
put(fruits2);
}
public static void put(Fruits<?> tmp){
System.out.println("这个水果是&#xff1a;"&#43;tmp.getFruits());
tmp.setFruits("banana");
tmp.setFruits(66);
Fruits ret &#61; tmp.getName();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cXPRUoxj-1665319877791)(C:\Users\COFFEEWEN\AppData\Roaming\Typora\typora-user-images\image-20221009200552338.png)]
解释&#xff1a;
-
注释1&#xff1a;处 put(Fruits> tmp)
使用无边界通配符&#xff0c;说明它是可以接受任意类型&#xff0c;所以当不同类型的fruits1和fruits2
调用put进行传参时&#xff0c;并不会报错。
-
注释2处、3处&#xff1a;由于是可以接受任意类型的&#xff0c;tmp是不确定类型的&#xff0c;不允许进行修改操作。比如&#xff0c;你此时传入了一个String类型的(传参时不会报错)&#xff0c;可是你代码里写的是tmp.setFruits(66)
设置的是int型&#xff0c;这样是很不安全的&#xff0c;所以编译器直接不允许你这么写。
-
注释4处&#xff1a;传入的参数类型是不确定的&#xff0c;不能用Fruits来接收。
5.1通配符上界
class Apple extends Fruits{
}
class Banana extends Fruits{
}
class Fruits extends Meat{
}
class Meat{
}
class Food<T>{
private T name;
public T getName(){
return this.name;
}
public void setName(T name){
this.name &#61; name;
}
}
public class Demo {
public static void main(String[] args) {
Food<Apple> appleFood &#61; new Food<>();
appleFood.setName(new Apple());
fun(appleFood);
Food<Banana> bananaFood &#61; new Food<>();
bananaFood.setName(new Banana());
fun(bananaFood);
}
public static void fun(Food<? extends Fruits> tmp){
System.out.println(tmp.getName());
tmp.setName(new Banana());
tmp.setName(new Apple());
Fruits ret &#61; tmp.getName();
}
解释&#xff1a;
- 注释1处&#xff1a;使用了上界通配符&#xff0c;表示可以传入的实参类型是Fruits和Fruits的子类型。
- 注释2、3处&#xff1a;由于是不确定类型的&#xff0c;所以无法修改&#xff0c;因为tmp接收的是Fruits和它的子类&#xff0c;此时存储的元素类型应该是哪个子类&#xff0c;是无法确定的&#xff0c;所以设置会报错&#xff01;但是可以获取元素&#xff0c;因为ret的类型的Fruits&#xff0c;是所有能传入参数的父类。
- 通配符上界&#xff1a;可以读取数据&#xff0c;不能写入数据
5.2通配符下界
public static void main(String[] args) {
Food f1 &#61; new Food<>();
f1.setName(new Fruits());
fun(f1);
Food f2 &#61; new Food<>();
f2.setName(new Meat());
fun(f2);
}
public static void fun(Food super Fruits> tmp){//1.
System.out.println(tmp.getName());//只能直接输出
tmp.setName(new Banana());//2.可以修改
tmp.setName(new Fruits());//3.可以修改
Fruits ret &#61; tmp.getName();//4.不能读取数据
}
解释&#xff1a;
- 注释1处&#xff1a;使用的是通配符下界&#xff0c;表示接受的参数只能是Fruits或者是Fruits的父类。
- 注释2、3处&#xff1a;可以修改&#xff0c;因为tmp的类型是Fruits或者它的父类&#xff0c;只能设置Fruits的子类或者它本身。
- 注释4处&#xff1a;如果此时传入的参数是Fruits的父类&#xff0c;用Fruits类型来接收是不安全的&#xff0c;换句话说&#xff0c;传入的参数是不确定是哪个父类&#xff0c;不能接收。
- 通配符下界&#xff1a;不能读取数据&#xff0c;可以写入数据。
总结 | |
---|
>无边界通配符 | 可以传入任意实参类型&#xff0c;不能写入&#xff0c;不能读取&#xff0c;只能输出。 |
extends 上界> | 可以传入实参类型是上界或者上界的子类&#xff0c;不能写入&#xff0c;可以读取。 |
super 下界> | 可以传入实参类型是下界或者下界的父类&#xff0c;不能读取&#xff0c;可以写入数据。 |
配符下界&#xff1a;不能读取数据&#xff0c;可以写入数据。
总结 | |
---|
>无边界通配符 | 可以传入任意实参类型&#xff0c;不能写入&#xff0c;不能读取&#xff0c;只能输出。 |
extends 上界> | 可以传入实参类型是上界或者上界的子类&#xff0c;不能写入&#xff0c;可以读取。 |
super 下界> | 可以传入实参类型是下界或者下界的父类&#xff0c;不能读取&#xff0c;可以写入数据。 |
有坑填坑
public static void main(String[] args) {
Integer a &#61; 100;
Integer b &#61; 100;
Integer c &#61; 200;
Integer d &#61; 200;
System.out.println(a &#61;&#61; b);
System.out.println(c &#61;&#61; d);
}
解释&#xff1a;
- 注释1处&#xff1a;输出true&#xff1b;
- 注释2处&#xff1a;输出false&#xff1b;
- Why&#xff1f;
Integer
源码有一个cache
数组事先存储了【-128,127】之间的数据&#xff0c;总共256个数据&#xff0c;这些数据被static final
修饰&#xff0c;是不能改变的。当赋的值是在这个区间里&#xff0c;会直接在这个数组里读取&#xff0c;也就是变量a和b&#xff0c;指向了同一个对象&#xff0c;所以输出true
。当超过了这个区间&#xff0c;每次都会重新new一个对象&#xff0c;所以输出false
。