1. 程式人生 > >第三十四講 初次認識泛型

第三十四講 初次認識泛型

泛型的簡單概述

泛型是JDK1.5版本以後出現的新特性。它用於解決安全問題,是一個型別安全機制。

泛型的由來

概念說完之後,我們來看看Java語言是如何引入泛型的。在JDK1.4版本之前,容器什麼型別的物件都可以儲存,但是在取出時,需要用到物件的特有內容時,這時需要做向下轉型。比如下面的程式:

public class MyGenericDemo {
    public static void main(String[] args) {
        List list = new ArrayList();

        list.add("abc");
        list.
add(4); // list.add(Integer.valueOf(4)); 由於集合沒有做任何限定,任何型別都可以存放在其中 for (Iterator it = list.iterator(); it.hasNext();) { String str = (String) it.next(); System.out.println(str.length()); } } }

程式在執行的時候發生了異常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

,其主要原因是物件的型別不一致。
為了避免這個問題,所以程式設計師們只能主觀上控制,往集合中儲存的物件型別保持一致。JDK1.5以後解決了該問題,即在定義集合時,就直接明確集合中儲存元素的具體型別,這樣,編譯器在編譯時,就可以對集合中儲存的物件型別進行檢查,一旦發現型別不匹配,就編譯失敗。這個技術就是泛型技術。

泛型的格式

泛型的格式是通過<>來定義要操作的引用資料型別。

泛型的好處

泛型的好處:

  1. 將執行時期出現的問題(例如ClassCastException異常),轉移到了編譯時期,方便於程式設計師解決問題,讓執行時期問題減少,安全;
  2. 避免了向下轉型的麻煩;
  3. 優化了程式設計,解決了黃色警告線。

總結:泛型就是應用在編譯時期的一項安全機制。

泛型的擦除

泛型的擦除用一句話來說就是:編譯器通過泛型對元素型別進行檢查,只要檢查通過,就會生成class檔案,但在class檔案中,就將泛型標識去掉了。

泛型的表現

泛型技術在集合框架中應用的範圍很大。那麼什麼時候需要寫泛型呢?只要看到類或介面在描述的時候右邊有定義<>,就需要泛型了。其實是容器在不明確操作元素的型別的情況下,對外提供了一個引數<>,使用容器時,只要將具體的型別實參傳遞給該引數即可。說白了,泛型就是傳遞型別引數。

使用泛型技術儲存整數

例,建立一個List集合,用於儲存整數,使用泛型技術。

package cn.liayun.generic.demo;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericDemo2 {

	public static void main(String[] args) {
		//建立一個List集合,儲存整數。List介面和ArrayList類
		List<Integer> list = new ArrayList<Integer>();
		
		list.add(5);
		list.add(6);
		
		for (Iterator<Integer> it = list.iterator(); it.hasNext();) {
			Integer integer = it.next();
			System.out.println(integer);
		}
	}

}

使用泛型技術儲存自定義物件

例,將Person物件儲存到TreeSet集合中,同姓名同年齡的視為同一個人,不存;按照學生的姓名進行升序排序,而且當姓名相同時,需要按照學生的年齡進行升序排序。

分析:此題主旨就是複習一下TreeSet集合和Comparator介面,我們定義一個比較器實現Comparator介面,覆蓋compare方法,將Comparator介面的實現類作為引數傳遞給TreeSet集合的建構函式。

先描述Person類,由於Person類的物件有可能存放到HashSet集合中,所以我們最好還是覆蓋hashCode()和equals()方法。而且我們還讓Person類實現了Comparable介面,強制讓Person類具備比較性。

package cn.liayun.domain;

public class Person implements Comparable<Person> {
	private String name;
	private int age;

	public Person() {
		super();
	}

	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
	
	@Override
	public int hashCode() {
		final int NUMBER = 38;
		return this.name.hashCode() + age * NUMBER;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (!(obj instanceof Person)) {
			throw new ClassCastException("型別錯誤");
		}
		Person per = (Person)obj;
		return this.name.equals(per.name) && this.age == per.age;
	}

	@Override
	public int compareTo(Person o) {
		int temp = this.age - o.age; 
		return temp == 0 ? this.name.compareTo(o.name) : temp;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
}

接著自定義一個比較器。

package cn.liayun.comparator;

import java.util.Comparator;

import cn.liayun.domain.Person;

public class ComparatorByName implements Comparator<Person> {

	@Override
	public int compare(Person o1, Person o2) {
		int temp = o1.getName().compareTo(o2.getName());
		return temp == 0 ? o1.getAge() - o2.getAge() : temp;
	}

}

最後編寫一個測試類,進行測試。

package cn.liayun.generic.demo;

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

import cn.liayun.comparator.ComparatorByName;
import cn.liayun.domain.Person;

public class GenericDemo3 {

	public static void main(String[] args) {
		Set<Person> set = new TreeSet<Person>(new ComparatorByName());
		
//		set = new HashSet<Person>();
		
		set.add(new Person("abcd", 20));
		set.add(new Person("abcd", 20));
		set.add(new Person("aa", 26));
		set.add(new Person("nba", 22));
		set.add(new Person("nba", 22));
		set.add(new Person("cba", 24));
		
		for (Person person : set) {
			System.out.println(person);
		}
	}

}

自定義泛型類

現在有兩個類,分別為Student和Worker,如下:

  • Student.java

    package cn.liayun.domain;
    
    public class Student extends Person {
    
    	public Student() {
    		super();
    	}
    
    	public Student(String name, int age) {
    		super(name, age);
    	}
    
    	@Override
    	public String toString() {
    		return "Student [name = " + getName() +  ", age = " + getAge() + "]";
    	}
    	
    }
    
  • Worker.java

    package cn.liayun.domain;
    
    public class Worker extends Person {
    
    	public Worker() {
    		super();
    	}
    
    	public Worker(String name, int age) {
    		super(name, age);
    	}
    
    	@Override
    	public String toString() {
    		return "Worker [name=" + getName() + ", age=" + getAge() + "]";
    	}
    	
    }
    

現在我們需要建立一個用於操作Student物件的工具類,對物件進行設定和獲取。其實想都不會想,我們就會這麼做:

class Tool {
	private Student stu;

	public Student getStu() {
		return stu;
	}

	public void setStu(Student stu) {
		this.stu = stu;
	}
}

可是發現太有侷限性了,這時我們就要想可不可以定義一個可以操作所有物件的工具呢?答案是可以的,當要操作的物件型別不確定的時候,為了擴充套件,可以使用Object型別來完成,但是這種方式有一些小弊端,會出現向下轉型,向下轉型容易在執行時期發生java.lang.ClassCastException。下面就是JDK1.4版本時(此時泛型技術還沒出現)的程式碼:

//JDK1.4
class Tool {
	private Object obj;

	public Object getObj() {
		return obj;
	}

	public void setObj(Object obj) {
		this.obj = obj;
	}
	
}

JDK1.5以後,新的解決方案出現了。當型別不確定時,可以對外提供引數,由使用者通過傳遞引數的形式完成型別的確定。這樣工具類的程式碼就可這樣寫為:

//在類定義時就明確引數,由使用該類的呼叫者來傳遞具體的型別。
class Util<W> {//泛型類
	private W obj;

	public W getObj() {
		return obj;
	}

	public void setObj(W obj) {
		this.obj = obj;
	}
}

以上就是一個自定義的泛型類。

小結

什麼時候定義泛型類呢?當類中要操作的引用資料型別不確定的時候,早期定義Object來完成擴充套件,現在定義泛型來完成擴充套件。

泛型方法

泛型類定義的泛型,在整個類中有效。如果被方法使用,那麼泛型類的物件明確要操作的具體型別後,所有要操作的型別就已經固定了,為了讓不同方法可以操作不同型別,而且型別還不確定,那麼可以將泛型定義在方法上。特殊之處:靜態方法不可以訪問類上定義的泛型。如果靜態方法操作的引用資料型別不確定,可以將泛型定義在方法上。

package cn.liayun.generic.demo;

public class GenericDemo {

	public static void main(String[] args) {
		Demo<String> d = new Demo<String>();
		d.show("abc");
		d.print("abc");
		d.print(6);
	}

}

class Demo<W> {
	public /*static*/ void show(W w) {//靜態方法是無法訪問類上定義的泛型的。如果靜態方法需要定義泛型,泛型只能定義在方法上。
		System.out.println("show: " + w);
	}
	
	public static <A> void staticShow(A a) {
		System.out.println("static show: " + a);
	}
	
	public <Q> void print(Q w) {//泛型方法
		System.out.println("print: " + w);
	}
}

泛型介面

泛型可定義在介面上。如:

interface Inter<T> {//泛型介面
	public void show(T t);
}

泛型介面有兩種實現方式,其中一種實現方式為:

package cn.liayun.generic.demo;

public class GenericDemo {

	public static void main(String[] args) {
		InterImpl in = new InterImpl();
		in.show("hello generic type");
	}

}

interface Inter<T> {//泛型介面
	public void show(T t);
}

class InterImpl implements Inter<String> {

	@Override
	public void show(String t) {
		System.out.println(t);
	}
	
}

第二種實現方式為:

package cn.liayun.generic.demo;

public class GenericDemo6 {

	public static void main(String[] args) {
		SubDemo d = new SubDemo();
		d.show("abc");
	}

}

interface Inter<T> {//泛型介面
	public void show(T t);
}

class InterImpl<W> implements Inter<W> {

	@Override
	public void show(W t) {
		System.out.println("show: " + t);
	}
	
}

class SubDemo extends InterImpl<String> {
	
}

泛型萬用字元

泛型是在限定資料型別,當在集合或者其他地方使用到泛型後,那麼這時一旦明確泛型的資料型別,那麼在使用的時候只能給其傳遞與資料型別相匹配的型別,否則就會報錯。
在這裡插入圖片描述
上面呼叫方法語句屬於語法錯誤,因為泛型限定不一致。方法要的是Collection<Object>型別,但傳入的是List<Student>或者Set<String>,二者型別不匹配。
上述定義的printCollection方法中,由於定義的是列印集合的功能,應該是可以列印任意集合中的元素的。但定義方法時,根本無法確定具體集合中的元素型別是什麼。為了解決這個”無法確定具體集合中的元素型別”問題,Java中為我們提供了泛型的萬用字元<?>。對上面的方法,進行修改後,實現了可迭代任意元素型別集合的方法。

private static void printCollection(Collection<?> coll) {//在不明確具體型別的情況下,可以使用萬用字元來表示。
	for (Iterator<?> it = coll.iterator(); it.hasNext();) {
		Object obj = it.next();
		System.out.println(obj);
	}
}

總結一下:當使用泛型類或者介面時,傳遞的資料中,泛型型別不確定,可以通過萬用字元<?>表示。但是一旦使用泛型的萬用字元後,只能使用Object類中的共性方法,集合中元素自身方法無法使用。

泛型的上限

泛型的限定有兩種表現形式,上限和下限。其中上限的格式為:

? extends E:可以接收E型別或者E型別的子型別,泛型的上限

現在我們來演示泛型的上限在API中的體現,以TreeSet的建構函式——TreeSet(Collection<? extends E> c)為例來詳細講解泛型的上限。
TreeSet集合中有一個這樣的構造方法:

public TreeSet(Collection<? extends E> c) { }

根據該構造方法可以構造一個帶有內容的TreeSet集合,該例中所涉及到的3個類——Person、Student、Worker前文都有描述,繼承體系為:
在這裡插入圖片描述

package cn.liayun.generic.demo;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeSet;

import cn.liayun.domain.Person;
import cn.liayun.domain.Student;

public class GenericDemo9 {

	public static void main(String[] args) {
		/*
		 * 演示泛型限定在api中的體現。
		 * TreeSet的建構函式。
		 * TreeSet(Collection<? extends E> coll);
		 * 
		 * 什麼時候會用到上限呢?
		 * 一般往集合中儲存元素時,如果集合中定義了E型別,通常情況下應該儲存E型別的物件。
		 * 對於E的子型別的物件,E型別也可以接受,所以這時可以將泛型從E改為? extends E。
		 */
		Collection<Student> coll = new ArrayList<Student>();
		coll.add(new Student("abc1", 21));
		coll.add(new Student("abc2", 22));
		coll.add(new Student("abc3", 23));
		coll.add(new Student("abc4", 24));
		
		
		TreeSet<Person> ts = new TreeSet<Person>(coll);
		ts.add(new Person("abc8", 21));
		for (Iterator<Person> it = ts.iterator(); it.hasNext();) {
			Person person = it.next();
			System.out.println(person.getName());
		}
		
	}

}

class MyTreeSet<E> {
	MyTreeSet() {
		
	}
	MyTreeSet(Collection<? extends E> c) {
		
	}
}

小結

什麼時候會用到泛型上限呢?一般往集合中儲存元素時,如果集合定義了E型別,通常情況下應該儲存E型別的物件。對E的子型別的物件也可以接收,所以這時可以將泛型從E改為? extends E。

泛型的下限

下限的格式為:

? super E:可以接收E型別或者E的父型別,泛型的下限

現在我們來演示泛型的下限在API中的體現,以TreeSet的建構函式——TreeSet(Comparator<? super E> comparator)為例來詳細講解泛型的下限。
TreeSet集合中有一個這樣的構造方法: