一、为什么需要泛型?
首先,让我们来看一段代码:
1 public class Test {
2
3 public static void main(String[] args) {
4 List list = new ArrayList();
5 list.add("hello");
6 list.add("word");
7 list.add(100);
8
9 for (int i = 0; i < list.size(); i++) {
10 String str = (String) list.get(i); //此处发生向下转型
11
12 //"hello"和"word"本身就是String类对象,可以强制转换为String类
13 //100不是String类的实例化对象,不能强制转换为String类,因此会有ClassCastException异常
14
15 System.out.println("string:" + str);
16 }
17 }
18 }
定义了一个List类型的集合,此时默认为Object类型,它可以接收任意类型的参数,当对象放入集合时默认为Object类,所以编译阶段不会报错,但是当代码执行时,需要取出集合中的对象,此时对象就会变为自身的类型,因此会有 ClassCastException异常,指的是两个没有关系的对象进行强转出现的异常。
为了解决类似的问题,在JDK1.5中引入了一个新特性——泛型(generics)
泛型机制将类型转换时的类型检查从运行时提前到了编译时,避免出现ClassCastException异常,有效解决了上述问题。
因此,使用泛型编写的代码比使用object类并在需要时再强制类型转换的机制具有更好的可读性和安全性。
二、什么是泛型?
1.定义
泛型,即“参数化类型”,顾名思义,就是将类型定义成参数形式,在类定义的时候并不会设置类中的属性或方法中的参数的具体类型,而是在类使用时进行定义。
2.使用方式
在泛型的使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口、方法中;分别被称为:
- 泛型类
- 泛型接口
- 泛型方法
3.作用
使用泛型的动机是在编译时检测除错误
在出现泛型机制之前,想要实现一个类可以接收不同类型的参数,就必须使用Object类,利用继承机制(所有类都继承于Object类), 接收不同类的对象,然后在具体使用时,强制向下转型,如果两个对象之间没有关系,编译时不会报错,而运行时就会出现 ClassCastException异常,由此可见,向下转型是不安全的操作,存在一定的隐患,尽量避免使用向下转型。
泛型的出现彻底改变了向下转型的需求,引入泛型后,如果明确设置了类型,则为设置类型;如果没有设置类型,则默认为Object类型。
三、泛型的使用
泛型有三种使用方式,分别为泛型类、泛型接口、泛型方法
1.泛型类
定义:如果一个类被 <T> 的形式定义,那么它就被称为是泛型类
泛型类基本语法:
class MyClass<T> {
T value1;
}
这里的<T>表示形式泛型类型,随后可以用一个实际具体类型来替换它,替换的泛型类型称为泛型实例化,其中形式泛型类型还可以使用其他大写字母表示
- T 代表一般的任何类
- E 代表 Element ,或者 Exception 异常
- K 代表 Key
- V 代表 Value ,通常与 K 一起配合使用
- S 代表 Subtype
泛型类实例:
public class Point<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Point<Integer> integerPoint = new Point<Integer>();//类的实例化,泛型类的对象
Point<String> stringPoint = new Point<String>();//类的实例化,泛型类的对象
integerPoint.set(30);
stringPoint.set("北纬38度");
System.out.println(integerPoint.get());//避免向下转型
System.out.println(stringPoint.get());
}
}
注意:泛型类中的类型参数<T>必须是一个类,所有的基本数据类型必须使用包装类
泛型类也可以引入多个类型参数
class MyClass<T,E> {
T value1;
E value2;//类中有两个不确定类型的变量,故需要引入两个形式泛型类型
public T getValue1() {
return value1;
}
public void setValue1(T value1) {
this.value1 = value1;
}
public E getValue2() {
return value2;
}
public void setValue2(E value2) {
this.value2 = value2;
}
}
public class Test {
public static void main(String[] args) {
MyClass<String,Integer> myclass = new MyClass<String,Integer>();
myclass.setValue1("hello");
myclass.setValue2(100);
System.out.println(myclass.getValue1()+myclass.getValue2());
}
}
注意:当使用类来创建对象或声明引用变量时,必须指定具体的类型
2.泛型接口
定义:如果一个接口被 <T> 的形式定义,那么它就被称为是泛型接口
泛型接口基本语法:
interface IMessage<T> { // 在接口上定义了泛型
public void print(T t) ;
}
子类实现泛型接口时,可以继续使用泛型,也可以在实现泛型接口时明确给出具体类型
泛型接口实例:
1)子类继续使用泛型
interface IMessage<T> { // 在接口上定义了泛型
public void print(T t) ;
}
class MessageImpl<T> implements IMessage<T> {//子类定义时继续使用泛型
@Override
public void print(T t) {
System.out.println(t);
}
}
public class TestDemo {
public static void main(String[] args) {
IMessage<String> msg = new MessageImpl() ;//向上转型
msg.print("Hello World");
}
}
2)子类实现泛型接口时给出具体类型
interface IMessage<T> { // 在接口上定义了泛型
public void print(T t) ;
}
class MessageImpl implements IMessage<String> {//给出了具体类型
@Override
public void print(String t) {
System.out.println(t);
}
}
public class TestDemo {
public static void main(String[] args) {
IMessage<String> msg = new MessageImpl() ;//向上转型
msg.print("Hello World");
}
}
注意:当使用接口来声明引用变量时,必须指定具体的类型
3.泛型方法
定义:如果一个方法被 <T> 的形式定义,那么它就被称为是泛型方法
泛型方法基本语法:
class MyClass{
public <T> void testMethod(T t) {
System.out.println(t);
}
}
泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的,而泛型类的类型参数写在类名后面
方法中的 T 被称为参数化类型,它不是运行时真正的参数
声明的类型参数,其实也是可以当作返回值的类型的。
泛型类可以和泛型方法共存
class MyClass<T>{ //泛型类
public void testMethod1(T t) { //返回值前面没有<T>不是泛型方法
System.out.println(t);
}
public <E> T testMethod2(E e) { //泛型方法
return e;
}
}
public class Test {
public static void main(String[] args) {
MyClass<String> myClass = new MyClass<>();
myClass.testMethod1("hello 泛型类"); //testMethod1不是泛型方法,只能传入String类对象
Integer i = myClass.testMethod2(100); //testMethod2方法可以传入任意类型参数
System.out.println(i);
}
}
上面代码中,MyClass <T> 是泛型类,testMethod1 是泛型类中的普通方法,而 testMethod2 是一个泛型方法。
而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。
泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不相干
注意:如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名
四、通配符
首先,让我们来看一段代码:
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(99);
fun(message); // 出现错误,只能接收String
}
public static void fun(Message<String> temp){
System.out.println(temp.getMessage());
}
}
上面这段代码中,虽然使用泛型类解决了ClassCastException异常问题,但是fun方法只能接收String类对象,并不能接收所有泛型类型,这种情况就需要使用通配符"?"来处理。
使用通配符实例:
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(55);
fun(message);
}
// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(Message<?> temp){
//temp.setMessage(100); 无法修改!
System.out.println(temp.getMessage());
}
}
此外, 通配符"?"有两个子通配符,分别为
- ?extends 设置泛型上限
- ?super 设置泛型下限
设置泛型上限:? extends Number,表示只能够设置Number或其子类,例如:Integer、Double等;
class Message<T extends Number> { // 设置泛型上限
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<Integer> message = new Message() ;
message.setMessage(55);
fun(message);
}
// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(Message<? extends Number> temp){
//temp.setMessage(100); 仍然无法修改!
System.out.println(temp.getMessage());
}
}
设置泛型下限:? super String,表示只能够设置String及其父类Object
class Message<T> {
private T message ;
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
}
public class TestDemo {
public static void main(String[] args) {
Message<String> message = new Message() ;
message.setMessage("Hello World");
fun(message);
}
public static void fun(Message<? super String> temp){
// 此时可以修改!!
temp.setMessage("bit!");
System.out.println(temp.getMessage());
}
}
上限可以用在声明,不能修改;而下限只能用在方法参数,可以修改内容!
五、类型擦除
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方
class MyClass<T,E>{
private T message;
private E text;
public E getText() {
return text;
}
public void setText(E text) {
this.text = text;
}
public T getMessage() {
return message;
}
public void setMessage(T message) {
this.message = message;
}
public void testMethod1(T t) {
System.out.println(t);
}
}
public class Test {
public static void main(String[] args) {
MyClass<String,Integer> myClass1 = new MyClass<>();
Class cls = myClass1.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getType());
}
}
}
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的
Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。