Java 基础之泛型

身为一个 Android 工程师的我是从培训班出身的,大家也都知道培训机构的情况,虽说 Android 开发是基于 Java 语言的,但是其实很多 Android 工程师都跟我一样,在草草的了解了一遍 Java 之后就投入到 Android 开发了,而且发现不用很深入的了解 Java 也能做一些开发,但是随着工作的深入和自己的提升,发现基础不好还真是不行,很多新东西出来了,会一些基本使用了,然后当你想探索一下它是如何实现的时候却发现,我他喵的竟然看不懂。比如 Retrofit 这个框架,这是目前仍然很流行的一个框架,功能强大还方便,源码也简单,简单到只有 37 个文件。但是就是这么不多的文件,我却无从下手,因为它是利用注解、反射、泛型等来实现的,这真的很尴尬。看来欠下的债果然是要还的,决心补习一下 Java 基础,就先从注解、反射、泛型这几个点先切入。


1、泛型的引出

泛型其实我们在使用 List 的时候就已经用到了,但是它出现的原因是什么,这个我们也得去想一想。假设这样一个场景,需要定义一个表示坐标的类,可以保存 x、y 坐标,但是这个坐标的值的类型是不确定的,有可能是整数,如 x = 10 , y = 20 ;也可能是小数,如 x = 5.5 , y = 6.6 ;还可能是字符串,如 x = “东经115度”, y = “北纬39度”。那么 x 和 y 的类型就是关键了,不能只是 int 型, double 型或者 String 型,要同时能保存这三种甚至更多类型的值,首先想到的肯定是把 x 、 y 都定义为 Object 型。根据这个场景来写一下代码。

class Point {
	private Object x;
	private Object y;

	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 GenericDemo {
	public static void main(String[] args) {
		Point mPoint = new Point();
		mPoint.setX(10);
		mPoint.setY(20);
		int x = (Integer) mPoint.getX();
		int y = (Integer) mPoint.getY();
		System.out.println("x坐标:" + x + ",y坐标:" + y);
	}
}

运行之后打印如下:


可以看到 x 和 y 成功的赋值并取出了,在这段代码中,我们定义了一个 Point 类,并有 x 和 y 两个 Object 类型的成员变量,赋值的时候并看不出来什么,在取值的时候因为它是 object 型的,定义的是一个 int 型的变量接收,所以我们需要向下转型为 Integer 。上面说 x 和 y 还可以保存 int 型和 String 型,试了一下也同样可以。

		Point mPoint = new Point();
		mPoint.setX(5.5);
		mPoint.setY(6.6);
		double x = (Double) mPoint.getX();
		double y = (Double) mPoint.getY();
		System.out.println("x坐标:" + x + ",y坐标:" + y);


		Point mPoint = new Point();
		mPoint.setX("东经115度");
		mPoint.setY("北纬39度");
		String x = (String) mPoint.getX();
		String y = (String) mPoint.getY();
		System.out.println("x坐标:" + x + ",y坐标:" + y);


输出如下:

事实证明这个 Point 对象确实可以保存不同类型的坐标,所以说把 x 、 y 定义为 Object 型成功解决了我们的问题,但是正是因为定义为 Object 型,也带来了问题,因为所有的对象都继承自 Object ,所以 Object 型的对象可以向下转型为任意类型。向上转型的目的是统一操作,向下转型的目的是要使用子类特殊的属性或方法,但是向下转型是一个非常不安全的操作,为什么这么说?修改一下代码:

		Point mPoint = new Point();
		mPoint.setX(10);
		mPoint.setY(20);
		String x = (String) mPoint.getX();
		String y = (String) mPoint.getY();
		System.out.println("x坐标:" + x + ",y坐标:" + y);

我们赋值时是 int 型,取出的时候用 String 型去接收,这样的代码在编译的时候并不会报错,因为我们刚才说的了 x 和 y 是Object 型的,所以可以向下转型为任意类型,但是一运行程序就报错了:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.qinshou.genericdemo.GenericDemo.main(GenericDemo.java:30)

这个异常表达得很清楚,类型转换错误, Integer 型不能转换为 String 型。这就是隐患,这样的错误我们更希望它是在编译时就能检测到而不是在运行时再发生异常,所以 Java 5 引入的泛型支持,就能很好的解决这个问题。

泛型的核心在于:类在定义的时候可以使用一个标记,这个标记表示类中属性或方法参数的类型,在使用时才动态设置类型。具体如何使用,其实就是在类后面加一个标记,将所有 Object 的地方替换为这个标记:

class Point<T> {
	private T x;
	private T y;

	public T getX() {
		return x;
	}

	public void setX(T x) {
		this.x = x;
	}

	public Object getY() {
		return y;
	}

	public void setY(T y) {
		this.y = y;
	}

}
注意这个 T 可以是任意字母,A、B、C 都可以,T 是 Type 的缩写,所以习惯上一般用 T ,在添加了这个标记之后,发现使用 Point 类的地方出现了警告:


Point is a raw type. References to generic type Point<C> should be parameterized:意思就是 Point 类是个原始类,未加工未指定参数的类,需要指定参数类型。

所以现在我们在使用 Point 类的时候需要指定参数类型:

public class GenericDemo {
	public static void mBin(String[] args) {
		Point<Integer> mPoint = new Point<Integer>();
		mPoint.setX(10);
		mPoint.setY(20);
		int x = (Integer) mPoint.getX();
		int y = (Integer) mPoint.getY();
		System.out.println("x坐标:" + x + ",y坐标:" + y);
	}
}
我们在使用 Point 类的时候指定 Point 类的参数类型为 Integer ,那么如果这时候传入 String 类型的参数呢?


The method setX(Integer) in the type Point<Integer> is not applicable for the arguments (String): setX(Integer) 方法不能接收 String 类型的参数。这样我们就可以在编译的时候就能检测到了。

public class GenericDemo {
	public static void main(String[] args) {
		Point<Integer> mPoint = new Point<Integer>();
		mPoint.setX(10);
		mPoint.setY(20);
		int x = mPoint.getX();
		int y = mPoint.getY();
		System.out.println("x坐标:" + x + ",y坐标:" + y);
	}
}
运行程序,结果如下:


而且因为规定了 Point 类的参数类型,所以我们取出数据的时候也不需要向下转型了,避免了类型强制转换可能发生的异常。

需要注意的几点:

1).指定参数类型的时候要使用引用类型而不能是基本类型,即不能使用 int 、 double 这样的指定方式,而要用 Integer 、 Double 这样的引用类型。

2).如果类中添加了泛型,但是还是没有指定泛型的话,那么还是会默认使用 Object 类型,这主要是为了兼容 Java 5 之前的程序。

3).Java 7 开始可以简化声明泛型,即 Point<Integer> mPoint = new Point<Integer>(); 后面的 Integer 可以省略,变成 Point<Integer> mPoint = new Point<>(); 但是一般为了代码完整,还是建议不要省略。


2、通配符

通配符使用的频率倒不是很高,那什么是通配符,先通过一个例子来说明为什么需要通配符:

定义一个 Message 类:

class Message<T> {
	private T message;

	public T getMessage() {
		return message;
	}

	public void setMessage(T message) {
		this.message = message;
	}

	@Override
	public String toString() {
		return "Message [message=" + message + "]";
	}

}


在 main() 方法中定义一个传入 Message 参数的方法:

public class GenericDemo {
	public static void main(String[] args) {
		Message<Integer> mMessage = new Message<Integer>();
		mMessage.setMessage(10);
		show(mMessage);
	}

	public static void show(Message<Integer> message) {
		System.out.println(message);
	}
}
运行程序,打印没问题:



假如我们现在实例化的 Message 对象不是 Integer 型呢,是一个 String 类型的,那么程序肯定会报错:


如果要解决这个问题,我们只能修改对应方法(即 show() 方法)的泛型,但是如果后面还需要换呢,换成 Double 、 Float 等,难道每次都要去修改方法吗?请注意,这样的情况是不能通过重载来解决的:


Erasure of method show(Message<Integer>) is the same as another method in type GenericDemo:说明 show() 已经被定义过了,因为重载只是判断参数的类型是否相同,而与泛型无关。这时候,我们就需要 <?> 通配符来解决问题了:

public class GenericDemo {
	public static void main(String[] args) {
		Message<String> mMessage = new Message<String>();
		mMessage.setMessage("Hello World");
		show(mMessage);
	}

	public static void show(Message<?> message) {
		System.out.println(message);
	}
}
重新运行,可以看到需要重复设置方法参数泛型的问题被解决了,正确输出了结果:


其实要解决这个问题,我们还可以不指定 Message 的泛型,但是这样会出现一个警告,跟上面的警告一样:


而且如果不指定泛型,那么我们就可以任意修改 message 的类型:

public class GenericDemo {
	public static void main(String[] args) {
		Message<String> mMessage = new Message<String>();
		mMessage.setMessage("Hello World");
		show(mMessage);
	}

	public static void show(Message message) {
		message.setMessage(10);
		System.out.println(message);
	}
}

上面提到,如果不指定泛型,那么默认为 Object , Object 当然是可以设置任意类型的。为了数据的安全,我们一般不希望数据能够被随意修改,使用 <?> 通配符可以避免这个问题:


因为 Message 的类型是不确定的,所以我们也不能随意设置为 Integer 或任意其他类型。 <?> 通配符也就保证了可以接收一个类的任意泛型类型,但是不能够修改,只能够取出。

<?> 通配符还有两个子通配符,<? extend 类>,设置泛型上限;<? super 类> ,设置泛型下限。简单举下例子:

public class GenericDemo {
	public static void main(String[] args) {
		Message<Integer> mMessage = new Message<Integer>();
		mMessage.setMessage(10);
		show(mMessage);
	}

	public static void show(Message<? extends Number> message) {
		System.out.println(message);
	}
}
这时表示 show() 方法接收的参数只能 Number 的子类,也就是 Integer 、 Double 、Float 等,如果换成 String 或其他非 Number 子类就会报错了,而且是在编译时报错。同理,如果 show() 方法修改一下:

	public static void show(Message<? super Integer> message) {
		System.out.println(message);
	}
这时候表示 show() 方法接收的参数只能 Integer 的父类,比如 Number 、 Object 等。

通配符在开发中使用得并不是非常多,但是遇到代码时一定要看得懂。


3、泛型接口

之前使用泛型都是在类中使用的泛型,其实定义接口的时候也可以使用泛型:
interface IMessage<T> {
	void print(T t);
}

有接口当然得有实现类, 这个接口的泛型我们应该什么时候设置呢,有两种方式。
方式一:实现类继续设置跟父类一样的泛型标记,实例化实现类对象的时候再设置泛型
class MessageImpl<T> implements IMessage<T> {

	@Override
	public void print(T t) {
		// TODO Auto-generated method stub
		System.out.println(t);
	}

}

public class GenericDemo {
	public static void main(String[] args) {
		MessageImpl<String> mMessageImpl = new MessageImpl<String>();
		mMessageImpl.print("Hello World!");
	}
}
结果如下:


方式二:实现类明确定义泛型类型

class MessageImpl implements IMessage<String> {

	@Override
	public void print(String t) {
		// TODO Auto-generated method stub
	}
}

public class GenericDemo {
	public static void main(String[] args) {
		MessageImpl mMessageImpl = new MessageImpl();
		mMessageImpl.print("Hello World!");
	}
}
结果仍然是一样的,就不贴图了,可以看到方式二明确定义了父接口的泛型类型,那么实例化对象的时候就不用再设置泛型了,两种方式各有千秋。


4、泛型方法

之前的泛型方法都是定义在使用了泛型的类中,但是其实泛型方法也可以定义在不支持泛型的类中,其实很简单,一个例子说明:
public class GenericDemo {
	public static void main(String[] args) {
		System.out.println(getMessage("Hello World !").getClass());
	}

	public static <T> T getMessage(T t) {
		return t;
	}
}

我们传入的 String,返回的也是 String 类型,如果传入 int 类型,返回的自然也就是 Integer 类型:
public class GenericDemo {
	public static void main(String[] args) {
		System.out.println(getMessage(10).getClass());
	}

	public static <T> T getMessage(T t) {
		return t;
	}
}

5、泛型擦除与转换

使用了泛型声明的类中应该带有泛型类型参数,一般为了严格是这样规定,但是上面也有提到如果声明了泛型,也可以在使用时不添加泛型类型参数,因为需要兼容 Java 5 及之前的代码,但是这样会带来一个问题,那就是当把一个带有泛型信息的对象赋给另一个没有泛型信息的变量时, <> 中的泛型信息就会被扔掉,这种现象叫做泛型擦除。
定义一个 Message 类,上限为 Number 类:
class Message<T extends Number> {
	private T message;

	public Message(T message) {
		super();
		this.message = message;
	}

	public T getMessage() {
		return message;
	}

	public void setMessage(T message) {
		this.message = message;
	}
}

虽然 mMessage1 指定了泛型,但是将 mMessage1 赋给没有指定泛型的 mMessage2,所以泛型被擦除了。这种情况在使用 List 的时候比较容易出现:
public class GenericDemo {
	public static void main(String[] args) {
		List<Integer> list1 = new ArrayList<Integer>();
		list1.add(10);
		list1.add(20);
		List list2 = list1;
		List<String> strList = list2;
		System.out.println(strList.get(0));

	}
}
这样的代码在编译时是没有问题的,但是一运行就会因为泛型擦除而报错:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at com.qinshou.genericdemo.GenericDemo.main(GenericDemo.java:30)
这样的情况称为“堆污染”,在注解的使用也会用到。

总结:泛型使用得很多,也很有用,其实对于泛型这个知识点我还是了解一些的,所以看起来还不是那么吃力,只是重新学一遍之后,更明白了它产生的原因,对它的分类也更清晰,对于今后使用泛型,也会更脚踏实地而不是只为了实现而写代码了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值