泛型的概念
- 所謂泛型姨拥,就是允許在定義類绅喉、接口時通過一個標(biāo)識表示類中某個屬性的類型或者是某個方法的返 回值及參數(shù)類型。這個類型參數(shù)將在使用時(例如叫乌,繼承或?qū)崿F(xiàn)這個接口柴罐,用這個類型聲明變量、 創(chuàng)建對象時確定(即傳入實(shí)際的類型參數(shù)憨奸,也稱為類型實(shí)參)革屠。
- 從
Java 5
以后,Java引入了“參數(shù)化類型(Parameterized type
)”的概念排宰,允許我們在創(chuàng)建集合時再指定集合元素的類型似芝,正如:List<String>
,這表明該List
只能保存字符串類型的對象板甘。 -
Java 5
改寫了集合框架中的全部接口和類党瓮,為這些接口、類增加了泛型支持盐类,從而可以在聲明集合變量寞奸、創(chuàng)建集合對象時傳入類型實(shí)參。
泛型的引入背景
集合容器類在設(shè)計階段/聲明階段不能確定這個容器到底實(shí)際存的是什么類型的對象在跳,所以在Java 5之前只能把元素類型設(shè)計為Object
蝇闭,Java 5之后使用泛型來解決。因?yàn)檫@個時候除了元素的類型不確定硬毕,其他的部分是確定的,例如關(guān)于這個元素如何保存礼仗,如何管理等是確定的吐咳,因此此時把元素的類型設(shè)計成一個參數(shù),這個類型參數(shù)叫做泛型元践。Collection<E>
韭脊,List<E>
,ArrayList<E>
這個<E>
就是類型參數(shù)单旁,即泛型沪羔。
引入泛型的目的
- 解決元素存儲的安全性問題,好比商品、藥品標(biāo)簽蔫饰,不會弄錯琅豆。
- 解決獲取數(shù)據(jù)元素時,需要類型強(qiáng)制轉(zhuǎn)換的問題篓吁,好比不用每回拿商品茫因、藥品都要辨別。
Java泛型可以保證如果程序在編譯時沒有發(fā)岀警告杖剪,運(yùn)行時就不會產(chǎn)生ClassCastException異常冻押。同時,代碼更加簡潔盛嘿、健壯洛巢。
泛型類
先看一個只能持有單個對象的類:
class Student {}
// 操作student表
public class Dao {// 表的共性操作,簡單的增刪改查
// 添加一條記錄
public void add(Student s){}
}
如果這時候需要操作teacher
表次兆,同樣需要添加一條記錄稿茉,那么這個時候就需要重新編寫Dao
類,這個類的可復(fù)用性就不高类垦,如果有很多表需要進(jìn)行這樣的操作狈邑,那么我們就需要為每一張表都單獨(dú)編寫一個新的Dao
類。
在Java 5之前蚤认,要解決這個問題米苹,我們可以使用Object
:
public class Dao {// 表的共性操作,簡單的增刪改查
// 添加一條記錄
public void add(Object obj){}
}
這樣又有了新問題砰琢,如果我現(xiàn)在向student
表插入一條Teacher
的信息蘸嘶,這也是可行了,并且不會報錯陪汽,因?yàn)?code>Object類是所有類的父類训唱。演示一下:
class Student { }
class Teacher { }
public class Dao { // 操作學(xué)生表的Dao
List<Object> stuList = new ArrayList<>();
// 向數(shù)據(jù)源中插入一條信息
public void add(Object obj) {
stuList.add(obj);
}
// 從數(shù)據(jù)源中取出數(shù)據(jù)
public Object get(int index) {
return stuList.get(index);
}
// 方便起見,直接在Dao類中寫單元測試方法挚冤,實(shí)際開發(fā)中不要這么寫
@Test
public void test() {
Dao stuDao = new Dao();
Student stu = new Student();
stuDao.add(stu); // 插入學(xué)生信息
Teacher teacher = new Teacher();
stuDao.add(teacher); // 沒有報錯况增,同樣可以插入
stuDao.add("垃圾數(shù)據(jù)"); // 同樣沒有報錯,這個問題就比較嚴(yán)重了训挡,什么類型的數(shù)據(jù)都能插入
// 取出數(shù)據(jù)
Student student = (Student) stuDao.get(0); // 需要強(qiáng)制類型轉(zhuǎn)換
Teacher teacher1 = (Teacher) stuDao.get(1); // 說出來你可能不信澳骤,我從學(xué)生表中取出了一個老師,混入了一個澜薄?为肮??
String str = (String) stuDao.get(2); // 我還從學(xué)生表里取出了一個字符串肤京?颊艳?
System.out.println(student);
System.out.println(teacher1);
System.out.println(str);
}
}
我通過Dao
這個類操作數(shù)據(jù)源中的student
表,然后分別向其中插入了Student
、Teacher
棋枕、String
三種類型的數(shù)據(jù)白修,這明顯是不符合要求的,student
表中的數(shù)據(jù)都被污染了戒悠,這對數(shù)據(jù)來說熬荆,是不安全的,沒有類型限制绸狐,我們很難保證永遠(yuǎn)不會寫錯卤恳,泛型的目的之一就是用來約定集合要存儲什么類型的對象,并通過編譯器確保條件滿足寒矿。
與其使用Object
突琳,我們更希望先指定一個類型占位符,稍后再決定具體使用什么類型符相。要達(dá)到這個目的拆融,需要使用類型參數(shù),用尖括號括住啊终,放在類名后面镜豹。然后在使用這個類時,再用實(shí)際的類型替換此類型參數(shù)蓝牲。
T
趟脂、K
、V
都可以表示類型參數(shù)例衍,任意字母都可以昔期。類型常用T
表示,是Type
的簡寫佛玄,Key
和Value
常用K
和V
表示硼一。
現(xiàn)在將上面的Dao
修改一下,T
表示類型參數(shù)(只能是對象類型梦抢,基本數(shù)據(jù)類型不行):
public class Dao<T> {
List<T> list = new ArrayList<>();
public void add(T t) {
list.add(t);
}
public T get(int index) {
return list.get(index);
}
@Test
public void test1() {
Dao<Student> stuDao = new Dao<>(); //在創(chuàng)建對象時般贼,指定參數(shù)類型
Student stu = new Student();
stuDao.add(stu); // 插入學(xué)生信息
//Teacher teacher = new Teacher();
//stuDao.add(teacher); // 報錯 會主動進(jìn)行類型校驗(yàn)
//stuDao.add("垃圾數(shù)據(jù)"); // 報錯
Student student = stuDao.get(0); // 無需進(jìn)行類型轉(zhuǎn)換
}
}
創(chuàng)建Dao
對象時,必須指明T
的類型奥吩,就像例子中T
的類型為Student
具伍,然后,只能在Dao
中存儲該類型(或其子類圈驼,因?yàn)槎鄳B(tài)和泛型不沖突)的對象了。當(dāng)你調(diào)用get(int index)
取值時望几,直接就是正確的類型绩脆。
Java泛型的核心概念:你只需要告訴編譯器要使用什么類型,剩下的細(xì)節(jié)交給它來處理。
在Java 5中靴迫,創(chuàng)建Dao
對象時惕味,你必須這么寫:
Dao<Student> stuDao = new Dao<Student>();
但是在Java 7及之后的版本,就可以這么寫:
Dao<Student> stuDao = new Dao<>(); //類型推斷
泛型接口
定義一個泛型接口:
public interface Dao<T> { // 與泛型類相似玉锌,在類名后面添加<T>名挥,參數(shù)類型
void add(T t);
T get(int index);
}
實(shí)現(xiàn)類:
class Student {
}
public class StudentDaoImpl implements Dao<Student> {
List<Student> stuList = new ArrayList<>();
@Override
public void add(Student student) {
stuList.add(student);
}
@Override
public Student get(int index) {
return stuList.get(index);
}
@Test
public void test() {
StudentDaoImpl studentDao = new StudentDaoImpl();
Student stu = new Student();
studentDao.add(stu);
Student student = studentDao.get(0);
System.out.println(student);
}
}
如果Teacher
有類似的操作,那么Dao
這個接口就可以直接被復(fù)用:
public class TeacherDaoImpl implements Dao<Teacher> {
List<Teacher> teacherList = new ArrayList<>();
@Override
public void add(Teacher teacher) {
teacherList.add(teacher);
}
@Override
public Teacher get(int index) {
return teacherList.get(index);
}
}
泛型方法
類本身可能是泛型的主守,也可能不是禀倔,和它的方法是否是泛型的沒有關(guān)系,泛型方法是獨(dú)立于類的参淫。如果方法是static
的救湖,則無法訪問該類的泛型類型參數(shù),因此涎才,如果使用了泛型類型參數(shù)鞋既,則它必須是泛型方法。
要定義泛型方法耍铜,請將泛型類型參數(shù)放在返回值之前邑闺,如下所示:
public class GenericMethods {
public <T> void f(T x) { // 泛型方法
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
盡管可以同時對類及其方法進(jìn)行參數(shù)化,但這里未將GenericMethods
類參數(shù)化棕兼。只有方法 f()
具有類型參數(shù)陡舅,該參數(shù)由方法返回類型之前的參數(shù)列表指示。
對于泛型類程储,必須在實(shí)例化該類時指定類型參數(shù)蹭沛。使用泛型方法時,通常不需要指定參數(shù)類型章鲤,因?yàn)榫幾g器會找出這些類型摊灭。 這稱為 類型參數(shù)推斷。因此败徊,對 f()
的調(diào)用看起來像普通的方法調(diào)用帚呼,并且 f()
看起來像被重載了無數(shù)次一樣。它甚至能接受GenericMethods
類型的參數(shù)皱蹦。
如果使用基本類型調(diào)用 f()
煤杀,自動裝箱就開始起作用,自動將基本類型包裝在它們對應(yīng)的包裝類型中沪哺。
下面的這兩種都不是泛型方法:
// 這不是泛型方法沈自,泛型方法的返回值前一定要有<T>
public void add(T t);
// 這個也不是泛型方法
public T get(int index);
需要注意的點(diǎn)
泛型類可能有多個參數(shù),此時應(yīng)將多個參數(shù)一起放在尖括號內(nèi)辜妓。比如
<E1,E2,E3>
-
泛型類的構(gòu)造器如下:
public GenericClass(){}
而下面是錯誤的:
public GenericClass<E>{}
實(shí)例化后枯途,操作原來泛型位置的結(jié)構(gòu)必須與指定的泛型類型一致滤灯。
-
泛型不同的引用不能相互賦值纺座。
盡管在編譯時 ArrayList<String>和ArrayList<Integer>是兩種類型,但是,在運(yùn)行時只有一個ArrayList被加載到JVM中渤滞。
-
泛型如果不指定识樱,將被擦除迂烁,泛型對應(yīng)的類型均按照
Object
處理恭陡,但不等價于Object
。建議:泛型要使用就都用坦报。要不用库说,就都不要用。
如果泛型結(jié)構(gòu)是一個接口或抽象類燎竖,則不可創(chuàng)建泛型類的對象璃弄。
JDK 7.0,泛型的簡化操作:
ArrayList<Fruit>first= new ArrayList<>();
(類型推斷)泛型的指定中不能使用基本數(shù)據(jù)類型构回,可以使用包裝類替換夏块。
在類/接口上聲明的泛型,在本類或本接口中即代表某種類型纤掸,可以作為非靜態(tài)屬性的類型脐供、非靜態(tài)方法的參數(shù)類型、非靜態(tài)方法的返回值類型借跪。但在靜態(tài)方法中不能使用類的泛型政己。
異常類不能是泛型的。
如果
Person
類是Student
類的父類掏愁,那么List<Person>
和List<Student>
不具備子父類的關(guān)系歇由,二者屬于并列關(guān)系。但是Person<String>
是Student<String>
的父類-
不能使用new E[]果港。但是可以:E[] elements= (E[])new Object[capacity];
參考:ArrayList源碼中聲明:Object[] elementData沦泌,而非泛型參數(shù)類型數(shù)組。
-
父類有泛型辛掠,子類可以選擇保留泛型也可以選擇指定泛型類型:
- 子類不保留父類的泛型:按需實(shí)現(xiàn)
- 沒有類型---擦除
- 具體類型
- 子類保留父類的泛型:泛型子類
- 全部保留
- 部分保留
- 結(jié)論:子類除了指定或保留父類的泛型谢谦,還可以增加自己的泛型
- 子類不保留父類的泛型:按需實(shí)現(xiàn)
代碼示例:
class Father<T1, T2> {
}
/**
* 定義泛型子類Son
* 情況一:繼承泛型父類后不保留父類的泛型
*/
//1.沒有指明類型 擦除
class Son1<A, B> extends Father {//等價于class Son1 extends Father<Object,Odject>{}
}
//2.指定具體類型
class Son2<A, B> extends Father<Integer, String> {
}
/**
* 定義泛型子類Son
* 情況二:繼承泛型父類后保留泛型類型
*/
//1.全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
}
//2.部分保留
class Son4<T2, A, B> extends Father<Integer,T2>{
}