目录
(六)泛型与注解
1、泛型:
(1)为什么要有泛型?
① 解决 “通用逻辑重复实现” 的问题,提升代码复用性
在没有泛型的场景中,要实现 “存储任意类型元素的集合” 或 “通用工具方法”,通常有两种低效方式:
用
Object
类型兼容所有类型:比如 Java 早期的ArrayList
内部用Object[]
存储元素,看似通用,却埋下了类型混乱的隐患;为每种类型重复写逻辑:比如为
String
写一个StringList
,为Integer
写一个IntegerList
,逻辑完全相同,只是类型不同,导致代码冗余。泛型通过类型参数(如
T
、K
、V
)解决了这个问题:定义时用占位符表示 “任意类型”,使用时再指定具体类型(如ArrayList<String>
、Map<Integer, User>
)。这意味着一套通用逻辑(如集合的 “添加”“获取” 操作)可以适配所有类型,无需重复编码。②实现 “编译时类型安全”,从源头减少运行时错误
没有泛型时,集合可以存储任意引用类型(如
String
、Integer
、Date
),但编译器无法判断开发者的真实意图,导致两个风险:
无意识的类型混乱:比如往 “本应存字符串的集合” 里塞数字(
list.add(123)
),编译器不报错,但运行时取出元素强转时会抛出ClassCastException
;错误只能在运行时暴露:类型不匹配的问题无法在开发阶段发现,可能直到线上运行才崩溃。
泛型将类型检查提前到编译期:通过
List<String>
明确告诉编译器 “此集合只能存String
”,一旦尝试添加其他类型(如int
),编译直接报错,从源头拦截错误。③避免 “冗余的强制转换”,让代码更简洁、高效
你可能会问:“集合存
Object
,直接用Object
类型变量不行吗?” 但实际开发中,我们几乎不会只把对象当Object
用 —— 我们需要调用其具体类型的方法(如String
的length()
、Integer
的intValue()
)。没有泛型时,从集合取元素必须手动强转(
String s = (String) list.get(0)
),既繁琐又容易写错;
有泛型时,编译器已知集合元素的具体类型(如List<String>
),取出时直接是String
类型(String s = list.get(0)
),可直接调用length()
,无需转换。这不仅让代码更简洁,还减少了运行时类型检查的性能开销。④ 支持 “可控的灵活性”,平衡通用与具体
泛型并没有剥夺集合 “存储多种类型” 的能力,而是让这种灵活性更可控:
存储同一类别的不同子类型:比如
List<Number>
可以存Integer
、Double
等所有数字类型,取出时无需强转成具体子类,直接用Number
的通用方法(如doubleValue()
);显式声明 “任意类型”:如果确实需要存完全无关的类型(虽然不推荐),可以用
List<Object>
,效果等同于不指定泛型,但代码意图更清晰(明确表示 “此处可能有多种类型”);通过通配符细化控制:用
? extends T
(上限)、? super T
(下限)进一步限制类型范围,比如void print(List<? extends Number> list)
可接收所有数字集合,既通用又安全。
(2)泛型的概述:
泛型(Generics)的概念是在JDK1.5中引入的,它的主要目的是为了解决类型安全性和代码复用的问题。
泛型是一种强大的特性,它允许我们在定义类、接口和方法时使用参数化类型 。
对泛型类型参数化时,只能表示引用数据类型
(3)泛型的应用:
接口源码:
//Collection接口定义,其是一个泛型接口
public interface Collection<E> {
//省略...
boolean add(E e);
}
Collection是一个泛型接口,泛型参数是E,add方法的参数类型也是E类型。在使用Collection接口的时候,给泛型参数指定了具体类型,那么就会防止出现类型转换异常的情况,因为这时候集合中添加的数据已经有了一个规定的类型,其他类型是添加不进来的。
泛型参数化:这里我们指定为String
Collection<String> c = new ArrayList<String>();
c.add("hello1");
c.add("hello2");
c.add("hello3");
//编译报错,add(E e) 已经变为 add(String e)
//int类型的数据1,是添加不到集合中去的
//c.add(1);
(4)自定义泛型:
<1>泛型类:
如果泛型参数定义在类上面,那么这个类就是一个泛型类
[修饰符] class 类名<泛型类型名1,泛型类型名2,...> {
0个或多个数据成员;
0个或多个构造方法;
0个或多个成员方法;
}
//注意:之前用确定数据类型的地方,现在使用自定义泛型类型名替代
实例化格式:
泛型类名<具体类型名1,具体类型名2……> 对象名 = new 泛型类名<>(实参);
举例:
//自定义泛型类:圆
//class 类名<泛型类型1,泛型类型2,...>
// 泛型类型名字可以自行定义
public class Circle<T, E> {
//原来具体数据类型的地方,使用泛型类型名替换即可
private T x;
private T y;
private E radius;
//无参构造器没有任何改变
public Circle() {}
//原来具体数据类型的地方,使用泛型类型名替换即可
public Circle(T x, T y, E radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
public E getRadius() {
return radius;
}
public void setRadius(E radius) {
this.radius = radius;
}
@Override
public String toString() {
return "Circle [x=" + x + ", y=" + y + ", radius=" + radius + "]";
}
}
//测试类:
package com.briup.chap08.test;
import com.briup.chap08.bean.Circle;
public class Test014_GenericsClass {
public static void main(String[] args) {
//实例化泛型类对象:
// 泛型类<具体类型1,具体类型2,...> 对象 = new 泛型类<>(实参s);
//1.实例化具体类对象,2种泛型设置为Integer和Double
// 注意,泛型类可以是任意引用类型
Circle<Integer, Double> c1 = new Circle<>(2,3,2.5);
int x = c1.getX();
double r = c1.getRadius();
System.out.println("x: " + x + " radius: " + r);
System.out.println("------------------");
//2.实例化具体类对象,2种泛型设置为Double和Integer
Circle<Double, Integer> c2 = new Circle<>(2.0,3.0,2);
double x2 = c2.getX();
int r2 = c2.getRadius();
System.out.println("x2: " + x2 + " r2: " + r2);
}
}
//运行结果:
x: 2 radius: 2.5
------------------
x2: 2.0 r2: 2
<2>泛型接口:
如果泛型参数定义在接口上面,那么这个接口就是一个泛型接口
定义格式:
[修饰符] interface 接口名<泛型类型名1,泛型类型名2,...> { }
举例:
public interface Action<T> {...}
public static void main(String[] args) {
//创建匿名内部类
Action<String> a = new Action<>() {
//...
};
}
<3>泛型方法:
如果泛型参数定义在方法上面,那么这个方法就是一个泛型方法
定义格式:
[修饰符] <泛型类型名> 返回值类型 方法名(形式参数列表){
方法具体实现;
}
调用格式:
类对象.泛型方法(实参列表);
类名.static泛型方法(实参列表);
注意:泛型方法调用时不需要额外指定泛型类型,系统自动识别泛型类型。
(5)注意事项:
//编译通过
//父类型的引用,指向子类对象
Object o = new Integer(1);
//编译通过
//Object[]类型兼容所有的【引用】类型数组
//arr可以指向任意 引用类型 数组对象
Object[] arr = new Integer[1];
//编译失败
//注意,这个编译报错,类型不兼容
//int[] 是基本类型数组
Object[] arr = new int[1];
//编译失败
//错误信息:ArrayList<Integer>无法转为ArrayList<Object>
//在编译期间,ArrayList<Integer>和ArrayList<Object>是俩个不同的类型,并且没有子父类型的关系
ArrayList<Object> list = new ArrayList<Integer>();
注意,= 号俩边的所指定的泛型类型(<>中所指定的类型),必须是要一样的
虽然 Integer 是 Object 的子类型,但是 ArrayList<Integer> 和 ArrayList<Object> 之间没有子父类型的关系,它们就是俩个不同的类型 。所以,也就是说,俩个类型,如果是当做泛型的指定类型的时候,就没有多态的特点了
(6)通配符:?
他可以表示任意的引用数据类型
<1>应用:在方法参数中用通配符,则表示可以传各种类型的集合
public void test(Collection<?> c) {
}
public static void main(String[] args){
Test t = new Test();
t.test(new ArrayList<String>());
t.test(new ArrayList<Integer>());
t.test(new ArrayList<Double>());
t.test(new ArrayList<任意引用类型>());
}
<2>通配符带来的问题:
通配符的 "只读" 特性:
Java 泛型的通配符(
?
)本质上是一种 "未知类型" 的表示,它有一个重要特性:只能读取,不能写入(除了null
)。这一特性导致了它在方法参数和类定义中表现不同。
当方法的参数是泛型类型为通配符的集合时,方法中只能读取元素(遍历或者get),不能添加会修改元素,只能添加null。
如果一个类的泛型参数是通配符(如class MyClass<?>
),会导致严重的使用限制,类内部的方法无法向泛型变量中写入任何具体类型的元素(除了null
),实例化时也无法指明集合存放的数据类型,使类失去意义,因此实际开发中绝对不会讲类的泛型参数定义为通配符。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("hello1");
list.add("hello2");
list.add("hello3");
list.add("hello4");
Collection<?> c = list;
//编译报错
//c.add("hello5");
for(Object obj : c) {
System.out.println(obj);
}
}
(7)泛型边界:
在默认情况下,泛型的类型是可以任意设置的,只要是引用类型就可以。
如果在泛型中使用 extends 和 super 关键字,就可以对泛型的类型进行限制,即规定泛型的上限和下限。
泛型的上限:
格式: 类型名<? extends 类型 > 对象名称
意义: 只能接收该类型及其子类
举例:
public static void main(String[] args) {
List<? extends Number> list;
//list可以指向泛型是Number或者Number【子】类型的集合对象
list = new ArrayList<Number>();
list = new ArrayList<Integer>();
list = new ArrayList<Double>();
//编译报错,因为String不是Number类型,也不是Number的子类型
//list = new ArrayList<String>();
}
泛型的下限:
格式: 类型名<? super 类型 > 对象名称
意义: 只能接收该类型及其父类型
(8)类型擦除:
泛型类型仅存在于编译期间,编译后的字节码和运行时不包含泛型信息,所有的泛型类型映射到同一份字节码。
由于泛型是JDK1.5才加入到Java语言特性的,Java让编译器擦除掉关于泛型类型的信息,这样使得Java可以向后兼容之前没有使用泛型的类库和代码,因为在字节码(class)层面是没有泛型概念的。
注意:因为他们的最终的字节码文件是相同的,因此最后无法构成重载。
总结:
2、注解:了解即可
(1)概述:
注解(Annotation),是JDK1.5引入的技术,用它可以对Java中的某一个段程序进行说明或标注,并且这个注解的信息可以被其他程序使用特定的方式读取到,从而完成相应的操作。
(2)注解的特性:
①声明方式:@符号开头
②可附加的位置:可以在类,方法,字段,参数,注解本身等几乎所有程序元素上
③作用:不直接执行逻辑,而是作为“标记”或配置,有其他工具(编译器,框架)解析并处理
(3)注释与注解的区别:
①注解是给其他程序看的,通过参数的设置,可以在编译后class文件中【保留】注解的信息,其他程序读取后,可以完成特定的操作
②注释是给程序员看的,无论怎么设置,编译后class文件中都是【没有】注释信息,方便程序员快速了解代码的作用或结构
(4)常见注解:
①内置注解:@Override 标记方法是对父类方法的重写,编译器直接验证重写是否正确
@SupperssWarnings 抑制编译器产生特定警告
②元注解:修饰注解的注解,一般用于定义注解中的行为(生命周期,适用范围)
@Target 指定注解可以附加的位置
@Target(ElementType.METHOD)
//指定该注解只能用于修饰方法
@interface MyAnnotation{……}
③自定义注解:通过@interface关键字定义,默认为public