明翰Java教学系列之泛型篇V0.2(持续更新)


传送门

  1. 明翰Java教学系列之认识Java篇

  2. 明翰Java教学系列之基础语法篇

  3. 明翰Java教学系列之初级面向对象篇

  4. 明翰Java教学系列之数组篇

  5. 明翰Java教学系列之进阶面向对象篇

  6. 明翰Java教学系列之异常篇

  7. 明翰Java教学系列之集合框架篇


前言

经常能看到各种源码&底层代码里充斥着这些东西:

public static <T extends Comparable<? super T>> void sort(List<T> list);  
public static <T> void sort(List<T> list, Comparator<? super T> c);  
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key);  
public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type );    

这些大写字母和尖括号到底是啥?这些就是泛型,"泛型"的"型"字代表数据类型的意思,
那泛型就跟数据类型有千丝万缕的联系了。
以前类型与变量是组合使用的,那么类型和变量是否可以分开呢?
类型是否可以像参数一样单独传递呢?

也有的同学知道泛型是什么,但不理解为什么要使用泛型,对泛型的概念很模糊,很正常,
毕竟泛型的使用场景没有像前面讲的集合框架使用那么频繁,但为了往一个更高的层次走,
能更顺理成章的阅读一些优秀项目源码,我们必须掌握泛型,今天就让我们来揭开泛型神秘的面纱。


什么是泛型

泛型(generic type&generics)是JDK5引入的特性,
从JDK5开始,java引入了参数化类型(parameterized type)机制,这种机制也被称为泛型。

泛型是对java数据类型体系的一种拓展,支持数据类型的传递。
泛型使数据类型可以像参数一样传递,在底层封装的时候很有用,
避免了强制转换,泛型多数用于编写底层模块或通用公共组件。

类型也可以传递?怎么传递呢?举几个小例子:

  1. a方法调用b方法的时候;
  2. a方法内实例化一个类,将一个类型传递到这个类中,
    类中的所有方法(包括方法的返回值类型与参数类型)和变量都可以使用这个类型;

理解泛型

集合框架中会大量的使用泛型机制,
因为集合框架中要承载着五花八门的数据类型。

在没有泛型的时代,集合框架是不记元素的数据类型的,
把所有元素的编译类型当成Object类型来处理(运行类型没有改变)。

并且当从集合框架中取出元素时,需要进行强制类型转换,
代码繁琐并且容易抛ClassCastException异常。

我们来看看最常用的Map,
如果没有使用泛型,我们一般会这么用:

Map m = new HashMap();
m.put("key", "alex");
String s = (String) m.get("key");

这段代码有什么痛点吗?

  1. 在get()时需要强制类型转换;
  2. 在put()时放入了非String类型,在get()时会抛异常,造成bug隐患;

每次都需要类型转换实在是很麻烦,
并且稍有疏忽就会造成异常的风险,实属下策。
那我们需要使用一种机制,来摒弃强制类型转换。

假设这个Map是由你来写的,
现在有n个人来调用你的put()与get(),
因为Map允许put()任何数据类型的数据,
但在写get()方法代码时的返回值与返回类型只能有一个。

作为开发者的你,你也不知道调用者传递的参数是什么类型,
A调用你可以给你传的是Integer类型,B给你传List类型,C给你传String类型。。。

你作为作者来说,你不知道他们会给你传什么类型,
就像你不知道他们会在什么时候调用你一样,
由于数据类型的多样化,要返回什么类型你自己也不知道,
因此一般我们会在写代码时把get()的返回类型写成Object,
因为Object是所有类的父类,
是所有类型的爸爸,具有最强的通用性。

但这样就会造成上面的2个痛点,如何避免这2个痛点呢?
我们需要引入今天的主角,使用泛型。
一般我们会这么玩Map的泛型:

Map<String,String> m = new HashMap<>();
//JDK7开始,构造器不需要带完整的泛型信息,只要给出一对尖括号即可,所谓的菱形语法。
m.put("key", "alex");
String s = m.get("key");

下面我们来看一下Map的源码,
第一眼看到K和V会纳闷,这到底是个啥啊?

public interface Map<K, V> {}
//让我们来对比一下源码和调用代码,发现相似的地方了咩?
Map<K, V>
Map<String,String>

我们貌似发现了相同之处,
就是K,V与String,String是相互对应的。

可以把这里的K和V理解成2个变量,这2个变量里放的东西就是数据类型, 可以叫他们类型变量。

第一个数据类型String传递给了K,
第二个数据类型String传递给了V。

可以理解成K=第一个String,V=第二个String。
在调用者声明Map变量的时候就已经把两个类型传递给了Map本身。

那么好,现在2个类型已经传递给Map了,传递过来了就得用对吧?怎么用呢?
我们再看一眼Map的源码,我们会发现K,V会在get()与put()时使用。

public interface Map<K, V> {
 public void put(K key, V value);
 public V get(K key);
}

在调用者调用get()时,
我们因为早就知道了要返回数据的类型,
就把get()要写返回类型的地方用V来代替,
因此愉快的就把正确的数据类型返回给调用方了。
此时还需要强制类型转换吗?不存在的。
这里的K,V相当于是把数据类型当做参数由调用者传递给了Map, 把类型参数化了,这就是所谓的类型传递。

我们可以说此处的Map是一个带类型参数的泛型接口,2个String就是类型参数。

这个K,V可以使用在很多地方,可以用于方法的参数列表所属数据类型,
也可以用于方法的返回值所属数据类型,
也可以用于类中的实例变量的所属数据类型。

使用泛型后,集合框架就可以记住元素的类型了。
在编译时就可以检查集合中元素的编译类型了,把错误扼杀在摇篮里,
出错率减少了,程序更健壮了。

使用泛型感觉还是很麻烦呢?

此时会有小伙伴们提出疑问:
“哎呀,用了泛型也要把类型事先定义好,跟强制类型转换差不多嘛,
为什么要多此一举呢?”

使用泛型机制后,如果你写了
Map<String,String>
而此时你put()的时候使用除了String之外的类型时就会报错,
编译时错误,IDE会报红叉子。
让程序员第一时间就知道,哦,类型传错了。
相当于是一层防护网,一层类型检查的防护网,
防止有人放错误的类型数据,把bug扼杀在摇篮里。

但如果使用强制类型转换,是不会报错的,等到运行的时候才会出错。
这也也是泛型的一个优点,能强制调用者的类型使用,相当于加了一个类型约束,
限制调用者必须使用这种类型,是不是代码出bug的几率减少了呢?
因此,使用泛型完美的解决了上述的2个痛点。


泛型的用法

泛型的用法也比较简单,在定义接口、类、方法时,使用尖括号<>来定义泛型信息,
尖括号里理论上写成啥都可以,就像你定义变量名字一样,
但约定俗成的写法是用单个大写字母作为类型参数名称,

尖括号中可以写入多个字母,用逗号分隔,
理论上你可以写成:

class test<A, B, C, D, E, F, G> {}

但这样写并不是很好对吧,一些常见的命名方式如下:
K=键;
V=值;
E=元素;
T=泛型;

使用尖括号里的字母来当做真正的数据类型来用即可,我们一般把它叫做类型形参
可以用于定义变量、方法参数、方法返回值等等。
这个数据类型将在创建对象、声明变量、调用方法时动态的由调用方动态传入,
定义传入的类型参数,我们一般叫它做类型实参
类似于方法实参、方法形参的概念。

换句话说,使用泛型了之后,
可以把类型这种东西,动态的传递了。。。

泛型类型可以定义在类上、接口上、方法上(包括方法参数类型与方法返回类型),
定义位置的不同,或多或少会有一些差异性。

调用泛型类型的时候,记住不要写基本数据类型,
只支持引用数据类型。

泛型类

一旦对类进行泛型化处理后(在类上定义泛型后,例如:
class XXX<T>),
类中的实例变量、方法(包括方法的返回类型与参数类型与方法体内的类型),
都可以获得并使用类的泛型类型(都可以使用这个T,把T当成具体类型)。

在实例化该类时需要指明泛型的具体类型,如果没有指明,则会得到警告,
将泛型类型当作Object来处理。

注意:
类变量&静态变量无法使用泛型类型,因为类变量是在类加载时期就入住内存并初始化,
而此时泛型类型还没有确定具体是哪种类型,因此无法使用,
静态块同理,而静态方法分两种情况,下面会有详细说明。

public class GenericsDemo1<T> {
	T info;

	public T getInfo() {
		return info;
	}

	public void setInfo(T info) {
		this.info = info;
	}

	public static void main(String[] args) {
		GenericsDemo1<String> g = new GenericsDemo1<>();
		// 实例变量的类型跟类的泛型类型走,是String类型。
		System.out.println(g.info);
		// 方法的参数类型跟类的泛型类型走,是String类型。
		g.setInfo("hello");
		// 方法的返回类型跟类的泛型类型走,是String类型。
		System.out.println(g.getInfo());
		
		GenericsDemo1<Integer> g2 = new GenericsDemo1<>();
		// 实例变量的类型跟类的泛型类型走,是Integer类型。
		System.out.println(g2.info);
		// 方法的参数类型跟类的泛型类型走,是Integer类型。
		g2.setInfo(123);
		// 方法的返回类型跟类的泛型类型走,是Integer类型。
		System.out.println(g2.getInfo());
	}
}

泛型接口

泛型接口与泛型类类似,但由于接口中所有的成员变量都是static的,
导致接口中的成员变量无法使用泛型类型。

public interface GenericsDemo2<T> {
	public T getInfo();
	public void setInfo(T info);
}

public interface Map<K,V> {
    V put(K key, V value);
    V get(Object key);
    Set<K> keySet();
}

泛型方法

上面讲了如何在类&接口中定义类型形参,
并在类&接口的内部像普通类型一样去使用这些类型形参。

那么在类&接口没有定义类型形参的情况下,方法能不能独立定义类型形参呢?
答案是肯定的,在定义方法时创建一个或多个方法自己的类型形参。
与普通方法相比,多了一个类型形参声明,需要放在修饰符与返回类型之间。

这个方法自己的类型形参只能被用于方法内部,像普通类型一样被使用。
方法自己的类型形参所属类型是不用显式的传递给方法的,编译器可自动获取。

泛型在方法中的使用有两种玩法,第一种是方法跟类的泛型类型走,
第二种是方法不跟类的泛型类型走。

public class GenericsDemo3<S> {
	public <T> T eat(T param) {
		// 该方法没有走类的泛型类型S,而是走当前方法自己的泛型类型T,一般来自方法的参数类型。
		// 如果不想走类的泛型方法,必须要在方法声明中写上<T>,否则编译报错。
		// 此处有没有类的泛型类型与当前方法都没有关系,即使没有类的泛型类型,这里也不会报错。
		return param;
	}

	public <T> T eat(T param1, T param2) {
		// 注意,如果走方法自己的泛型类型,在多个参数的情况下,
		// param1与param2的类型允许不一致,但如果是类的泛型类型走就必须一致。
		return param2;
	}

	public S sleep(S param) {
		// 该方法走类的泛型类型S。
		return param;
	}

	public static void main(String[] args) {
		// 类的泛型类型是String
		GenericsDemo3<String> g = new GenericsDemo3<>();
		// eat方法的泛型参数是Integer,跟方法参数走
		System.out.println(g.eat(6));
		// sleep方法的泛型参数是String,跟类走
		System.out.println(g.sleep("hello"));
		// eat方法的两个泛型参数允许不一致
		System.out.println(g.eat(true, 5));
	}
}
泛型静态方法

一些文章中会误导新人,说静态方法完全不能使用泛型,
这种说法是不严谨的,静态泛型方法分为两种情况:
1.如果静态方法使用<T>,则可以使用泛型,因为泛型类型跟方法自己走,跟类的泛型类型无关。
2.如果静态方法没用<T>,则不可以使用泛型,因为泛型类型跟类走,道理与类变量相似。
(我这里的T只是随便起的一个类型形参名字,并不是强制的)

public class GenericsDemo4<S> {
	public static <T> T eat(T param) {
		//虽然是类方法,但泛型类型与类无关,可以正常使用。
		return param;
	}
	
//	public static S sleep(S param) {
//	这种写法直接编译报错。
//		return param;
//	}
	
	public static void main(String[] args) {
		String alex = GenericsDemo4.eat("alex");
		System.out.println(alex);
	}
}
泛型构造方法
public class GenericsDemo5<S> {
	S info;

	public GenericsDemo5() {

	}

	public GenericsDemo5(S info) {
		this.info = info;
	}

	public <T> GenericsDemo5(T info1, T info2) {
		System.out.println(info1);
		System.out.println(info2);
		// 下面的泛型S与泛型T为互斥,会造成编译错误。
		// this.info = info1;
	}

	public static void main(String[] args) {
		GenericsDemo5<Boolean> g1 = new GenericsDemo5<>(true);
		System.out.println("----->"+g1.info);
		GenericsDemo5<Boolean> g2 = new GenericsDemo5<>("a", "b");
	}
}

泛型父子类继承

在父子类继承的场景下使用泛型,又分成两种情况:

1.子类没有定义子类的泛型类型
class Generics6Father<S> {
	public S info;

	public Generics6Father() {

	}

	public Generics6Father(S info) {
		this.info = info;
	}

	public S getInfo() {
		return info;
	}

	public void setInfo(S info) {
		this.info = info;
	} 
}

class Generics6Son extends Generics6Father<Integer> {
	//错误写法:class Generics6Son extends Generics6Father<T>
	//正确写法:class Generics6Son extends Generics6Father
	//如果子类没有给父类泛型,则默认为Object。
	public Integer getInfo() {
		//子类重写父类方法
		return info;
	}
	
//	public String getInfo() {
//		//子类重写父类方法,错误写法,应该是Integer类型
//		return info;
//	}
}
2.子类定义了子类的泛型类型
public class GenericsDemo7 {
	public static void main(String[] args) {
		// 父类泛型使用String
		GenericsFather<String> f = new GenericsFather<>();
		// info是String类型
		System.out.println(f.info);
		// getInfo()是String类型
		System.out.println(f.getInfo());
		System.out.println(f.eat(5));
		System.out.println(f.sleep("5"));

		// 子类泛型使用Integer
		GenericsSon<Integer> s = new GenericsSon<>();
		System.out.println(s.info);
		System.out.println(s.getInfo());
		System.out.println(s.eat(5));
		// 遇到直接继承自父类的方法则需要走父类的泛型Boolean
		System.out.println(s.sleep(true));

		// 多态的情况下
		GenericsFather<Boolean> ss = new GenericsSon<Integer>();
		// 此处info是Boolean类型,虽然子类定义了泛型类型Integer,
		// 但在多态下,调用的是父类的成员变量,会走父类的泛型类型。
		System.out.println(ss.info);
		System.out.println(ss.getInfo());
		System.out.println(ss.eat(5));
		System.out.println(ss.sleep(true));
	}
}

class GenericsFather<S> {
	public S info;

	public S getInfo() {
		return info;
	}

	public void setInfo(S info) {
		this.info = info;
	}

	public <A> A eat(A param) {
		return param;
	}

	public S sleep(S param) {
		return null;
	}
}

class GenericsSon<T> extends GenericsFather<Boolean> {
	public T info;

	public <S> S eat(S param) {
		return param;
	}

	public T eat2(T param) {
		return param;
	}
}
3.父子类继承注意

子类继承父类时,子类不能再给父类使用类型形参,必须传递类型实参。
允许子类不给父类传递类型实参,默认为Object类型。

class Generics10Father<T>{}
//错误
class GenericsDemo10 extends Generics10Father<T>{}
//正确
class GenericsDemo10 extends Generics10Father<String>{}
//正确
class GenericsDemo10 extends Generics10Father{}
//错误
class GenericsDemo10 extends Generics10Father<>{}

泛型通配符

在讲泛型通配符之前,我们需要先明确一个概念,
假设Father是Son的父类,但List<Father>绝对不是List<Son>的父接口&父类型,
这里要注意,关于父子的概念中泛型与数组是不一样的,请不要混淆。

那么如何来表示泛型类型的爸爸呢?
我们在使用List作为形参传递进方法后,如果必须要给这个形参加泛型,
但又不知道传递进来的参数是什么类型,该怎么办呢?

有些同学会说使用上面讲的泛型方法啊,没错是可以这么用,
但用的时机是否合适呢?我们后面会讲,那还有没有别的玩法呢?
答案是肯定的,就是我们现在要讲的泛型通配符。

什么是通配符?问号就是通配符,用问号来表示未知类型,
使用通配符来表示泛型类型的父类,可以匹配任何泛型类型,例子如下:

public class GenericsDemo10 {
	// 抱歉,由于List<Object>并不是一个父接口的概念,
	// 因此调用者传类似于List<String>是行不通的。
	public void eat1(List<Object> param) {
		for (int i = 0; i < param.size(); i++) {
			System.out.println(param.get(i));
		}
	}

	// 使用泛型通配符
	public void eat2(List<?> param) {
		for (int i = 0; i < param.size(); i++) {
			System.out.println(param.get(i));
		}
	}

	public static void main(String[] args) {
		GenericsDemo10 g = new GenericsDemo10();
		// 正确
		g.eat1(new ArrayList<Object>());
		// 错误
		// g.eat1(new ArrayList<String>());
		// 正确
		g.eat2(new ArrayList<String>());

		List<?> aaa = new ArrayList<>();
		// 错误,因为add()需要传递泛型类型E的对象或其子类对象,E是List集合中元素的类型,但现在无法知道E的类型是什么,因此出现编译错误。
		// aaa.add(new Object());
        //除了List<?>以外,还可以使用其他的接口例如:Set<?>,Collection<?>,Map<?,?>等等。
	}
}

需要注意的是,泛型通配符不能用于定义类的泛型类型。

泛型通配符与泛型方法

其实泛型通配符与泛型方法之间是可以互换的。
例如:

public class GenericsDemo12 {
	public void eat1(List<?> param) {
		//使用泛型通配符
	}

	public <T> void eat2(List<T> param) {
		//使用泛型方法
	}
	
	public <T> void sleep(List<T> a, List<? extends T> b) {
		// 使用泛型方法+泛型通配符
	}
}

那么什么时候使用泛型方法什么时候使用泛型通配符呢?
一般来说,只要是出现了类型依赖,就使用泛型方法,否则使用通配符。
当然也可以使用泛型方法+通配符的形式。

那什么是类型依赖呢?
类似于上面例子中的sleep()参数a和参数b之间就产生了类型依赖。

泛型限制类型(上限与下限)

前面提到的泛型都是无约束泛型,就是你想传什么类型都可以。
但有时我们可能要限制别人传递进来的泛型参数是某种类的子类或父类,
此时我们需要使用extends与super两个关键字来限制类型的传递。

<? extends T>,表示传递进来的类型是T本身或T的子类型,限制上限。
<? super T>,表示传递进来的类型是T本身或T的父类型,限制下限。

通配符形参上限:

public class GenericsDemo11 {
	public static void eat(List<? extends Number> param) {
		
	}
	
	public static void main(String[] args) {
		eat(new ArrayList<Number>());
		eat(new ArrayList<Integer>());
		eat(new ArrayList<Double>());
		// 编译出错,因为String并不是Number的子类
//		eat(new ArrayList<String>());
	}
}

类型形参上限:

public class GenericsDemo8<V extends Number> {
	//限制传递的泛型类型必须是Number的子类
	public static void main(String[] args) {
		GenericsDemo8<Integer> a = new GenericsDemo8<>();
		//Integer是Number的子类,运行正常
		GenericsDemo8<Double> b = new GenericsDemo8<>();
		//Double是Number的子类,运行正常
		//GenericsDemo8<String> c = new GenericsDemo8<>();
		//String不是Number的子类,编译错误
	}
}
 
class GenericsDemo88<V extends ArrayList & Collection & List> {
	// 可以用于限制多个接口,只能有一个父类上限,但可以有多个接口上限。类上限要写在第一位。
}

通配符形参下限:

public class GenericsDemo13 {
	public static void eat(List<? super Integer> param) {
		
	}
	
	public static void main(String[] args) {
		eat(new ArrayList<Number>());
		eat(new ArrayList<Integer>());
		// 编译出错,因为Double并不是Integer的父类
//		eat(new ArrayList<Double>());
		
	}
}

总结

泛型还是一个比较重要的知识点,让类型传递 成为可能,并让底层开发者可以对类型进行约束,让我们的程序更加健壮。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值