从JDK1.5以后引入了三大常用新特性:泛型、枚举(enum)、注解(Annotation)。其中JDK1.5中泛型是一件非常重要的实现技术,它可以帮助我们解决程序的参数转换问题。本文为大家详细介绍一下泛型。
一、泛型问题的引出
假设需要你定义一个描述坐标的程序类Point,需要提供两个属性x、y。对于这两个属性的内容可能有如下选择:
1. x = 10、y = 20 ;
2. x = 10.1、y = 20.1 ;
3. x = 东经80度、y = 北纬20度
那么现在首先要解决的问题就是Point类中的x、y的属性类型问题,此时需要保存的有int、double、String,所以在java中只有一种类型可以保存所有类型:Object型。
例:
class Point { //按照题目要求定义一个Point的类
private Object x ; //定义Object对象,接收不同类型的参数
private Object y ; //定义Object对象,接收不同类型的参数
public Object getX() {
return x;
}
public void setX(Object x) {
this.x = x;
}
public Object getY() {
return y;
}
public void setY(Object y) {
this.y = y;
}
}
当传入整型坐标时
public class Test {
public static void main(String[] args){
// 设置数据
Point p = new Point() ;
p.setX(10); // 自动装箱并且向上转型为Object
p.setY(20);
// 取出数据
int x = (Integer) p.getX() ; // 强制向下转型为Integer并且自动拆箱
int y = (Integer) p.getY() ;
System.out.println("x = " +x+",y = "+y);
}
}
当传入字符串数据类型时
public class Test {
public static void main(String[] args) {
// 设置数据
Point p = new Point() ;
p.setX("东经80度");
p.setY("北纬20度");
// 取出数据
String x = (String) p.getX() ;
String y = (String) p.getY() ;
System.out.println("x = " +x+",y = "+y);
}
}
当传入的是浮点数的时候
public calss Test {
public static void main(String[] args) {
//设置数据
Point p = new Point();
p.setX(10.1);// 自动装箱并且向上转型为Object
p.setY(20.1);
// 取出数据
double x = (Double) p.getX() ;// 强制向下转型为Double并且自动拆箱
double y = (Double) p.getY() ;
System.out.println("x = " +x+",y = "+y);
}
}
通过以上的实例,我们发现有两个不妥的地方:
不管是哪种类型的传入,必须保证x和y的类型一致,如果在主方法中传入数据时将x传double类型,y传String类型,则在编译时不会报错,但是在运行是会出现ClassCastException错误。(ClassCastException是指两个没有关系的对象进行强制转换的异常)
因此引入泛型
二、泛型类的语法和基本使用
泛型指的就是在类定义的时候并不会设置类中的属性或方法中的参数的具体类型,而是在类使用时再进行定义。如果要想进行这种泛型的操作,就必须做一个类型标记的声明。
例:
单参数参数泛型基本语法
class MyClass<T> {
T value1;
}
多参数泛型
class MyClass<T,E> {
T value1;
E value2;
}
泛型命名规则
尖括号 <> 中的 T 被称作是类型参数,用于指代任何类型。实际上这个T你可以任意写,但出于规范的目的,Java还是建议我们用单个大写字母来代表类型参数。常见的如:
T 代表一般的任何类。
E 代表 Element 的意思,或者 Exception 异常的意思。
K 代表 Key 的意思
V 代表 Value 的意思,通常与 K 一起配合使用。
S 代表 Subtype。
使用泛型类
//定义一个泛型参数的类
class MyClass {
T value1;
}
//主类中的使用
public class Test {
public static void main(String[] args) {
MyClass myClass1 = new MyClass();
MyClass myClass2 = new MyClass();
}
}
class MyClass {
T value1;
E value2;
}
public class Test {
public static void main(String[] args) {
MyClass myClass1 = new MyClass();
}
}
注意:泛型只能接受类,所有的基本数据类型必须使用包装类!
现在在用我们的泛型类来实现第一个Point类的例子
package www.bit.java.test;
class Point { // T表示参数,是一个占位的标记;如果有多个泛型就继续在后面追加
private T x ;
private T y ;
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 class TestDemo {
public static void main(String[] args) {
// 设置数据
Point p = new Point() ; // JDK1.5的语法
p.setX("东经80度");
p.setY("北纬20度");
// 取出数据
String x = p.getX() ; // 避免了向下转型
String y = p.getY() ;
System.out.println("x = " +x+",y = "+y);
}
}
注意:Point< String > p = new Point< String >() ;
此行语句在JDK1.7以后可以这么写:Point< String > p = new Point<>()
当开发的程序可以避免向下转型,也就意味着安全隐患被消除了。尽量不要去使用向下转型。
泛型的出现彻底改变了向下转型的需求。引入泛型后,如果明确设置了类型,则为设置类型;如果没有设置类型,则默认为Object类型。
三、泛型方法
class MyClass{
//返回值类型为void,参数类型为T
public <T> void testMethod1(T t) {
System.out.println(t);
}
//返回值类型为T,参数类型也为T
pubic <T> T testMethod2(T t) {
return t;
}
}
泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。 中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。
泛型方法与泛型类共存
例:
class MyClass{
public void testMethod1(T t) {
System.out.println(t);
}
public T testMethod2(T t) {
return t;
}
}
public class Test {
public static void main(String[] args) {
MyClass myClass = new MyClass<>();
myClass.testMethod1("hello 泛型类");
Integer i = myClass.testMethod2(100);
System.out.println(i);
}
}
上面代码中,MyClass 是泛型类,testMethod1 是泛型类中的普通方法,而 testMethod2 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,**泛型方法始终以自己定义的类型参数为准**。
泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不相干。但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
因此,上述代码这样写比较好
class MyClass{
public void testMethod1(T t) {
System.out.println(t);
}
public E testMethod2(E e) {
return e;
}
}
四、通配符
在程序类中追加了泛型的定义后,避免了ClassCastException的问题,但是又会产生新的情况:参数的统一问题。
例:
package www.bit.java.test;
class Message {
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 message = new Message() ;
message.setMessage("hello world");
fun1(message);
fun2(message);
}
//
public static void fun1(Message temp){
System.out.println(temp.getMessage());
}
//注意这里形参的泛型传的是Interage,那么上面传的是String类,调用之后就会出错
public static void fun2(Message temp){
SyStem.out.println(temp.getMessage());
}
}
我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使用通配符”?”来处理。
例:使用通配符
public class TestDemo {
public static void main(String[] args) {
Message message = new Message() ;
message.setMessage(55);
fun(message);
}
// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(Message> temp){
//temp.setMessage(100); 无法修改!
System.out.println(temp.getMessage());
}
}
在”?”的基础上又产生了两个子通配符:
? extends 类:设置泛型上限:
例如:? extends Number,表示只能够设置Number或其子类,例如:Integer、Double等;
? super 类:设置泛型下限:
例如:? super String,表示只能够设置String及其父类Object。
例:设置泛型上限
package www.bit.java.test;
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 message = new Message() ;
message.setMessage(55);
fun(message);
}
// 此时使用通配符"?"描述的是它可以接收任意类型,但是由于不确定类型,所以无法修改
public static void fun(Message extends Number> temp){
//temp.setMessage(100); 仍然无法修改!
System.out.println(temp.getMessage());
}
}
例:设置泛型下限
package www.bit.java.test;
class Message {
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 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());
}
}
注意:上限可以用在声明,不能修改;而下限只能用在方法参数,可以修改内容!
五、泛型接口
泛型除了可以定义在类中,也可以定义在接口里面,这种情况我们称之为泛型接口。
例:定义一个泛型接口
interface IMessage { // 在接口上定义了泛型
public void print(T t) ;
}
对于这个接口的实现子类有两种做法
例:在子类定义时继续使用泛型
package www.bit.java.test;
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 msg = new MessageImpl() ;
msg.print("Hello World");
}
}
例:在子类实现接口的时候明确给出具体类型
package www.bit.java.test;
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 msg = new MessageImpl() ;
msg.print("Hello World");
}
}
六、类型擦出
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
例:观察泛型擦出
class MyClass{
private T message;
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 myClass1 = new MyClass<>();
MyClass myClass2 = new MyClass<>();
System.out.println(myClass1.getClass() == myClass2.getClass());
}
}
运行结果为 true
打印的结果为 true 是因为 MyClass 和 MyClass 在 jvm 中的 Class 都是 MyClass.class。
例:
import java.lang.reflect.Field;
class MyClass{
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 myClass1 = new MyClass<>();
Class cls = myClass1.getClass();
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getType());
}
}
}
运行结果
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 则会被转译成普通的Object 类型,如果指定了上限如 则类型参数就被替换成类型上限。