泛型
当将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。
List list=new ArrayList();
list.add(new Random());
list.add("abc"); //list并没有识别"abc"是字符串类型,而是当作Object进行处理
String str=list.get(0);//语法错误,需要编程实现类型转换
注意:问题1进行数据的强制类型转换之前必须进行类型判断,ClassCastException
List list=new ArrayList();
list.add(new Random());
list.add("abc");
Object str=list.get(0);
if(str!=null && str instanceof String){
String ss=(String) str; //简单写法为String ss=str.toString()
}
因此取出集合元素时需要人为的强制类型转化到具体的目标类型,但是很容易出现ClassCastException异常,那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现java.lang.ClassCastException异常呢?
答案就是使用泛型,可以将运行时的类型检查搬到编译期实现
List<String> list=new ArrayList<>(); //<String>用于声明list中只能存放String类型的数据,如果类型不匹配则会编译失败
//list.add(new Random()) 语法报错,编译失败
list.add("abc");
String ss=list.get(0); //语法正确,不需要进行强制类型转换(窄化操作)
不使用泛型,使用集合时编程比较复杂
List list=new ArrayList();
list.add(123); //自动向上转型 int-->Integer-->Object
//获取数据还需要窄化操作
int kk=(Integer)list.get(0); //不进行类型判断,有可能出现ClassCastException
使用泛型,可以将运行时的类型检查搬到编译期实现;同时获取数据时不需要再编码进行类型转换
List<Integer> list=new ArrayList<>(); //从JDK1.7+支持泛型推导
list.add(123);
list.add(new Random());//编译报错
int kk=list.get(0); //不需要进行强制类型转换
什么是泛型
泛型是jdk5引入的类型机制,就是将类型参数化,它是早在1999年就制定的JSR14的实现。泛型作为一种安全机制而产生
public void pp(String str){} //str实际上是形参
public void pp(Integer str){}
调用时按照位置对应传入实际数据,实际数据叫做实参
pp("dddd")
泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比杂乱的使用object并在需要时再强制类型转换的机制具有更好的可读性和安全性。
泛型在本质上是指类型参数化。所谓类型参数化,是指用来声明数据的类型本身,也是可以改变的,它由实际参数来决定。在一般情况下,实际参数决定了形式参数的值。而类型参数化,则是实际参数的类型决定了形式参数的类型。
在声明阶段E是什么类型不确定,这里的E仅仅充当占位符的作用,在具体调用时类型才能确定,而E的所有位置将被指定的类型所替代
使用泛型的定义
public interface List<E> extends Collection<E> 这里的<>中的内容就是类型参数,一般建议使用T或者
E之类的全大写
{
E get(int index); 获取的元素类型就是定义时指定的类型
void add(E element);
}
List<String> list = new ArrayList<String>();
// list.add(new Random()) 编译失败,语法报错,因为E现在是String类型,不是Random
list.add("ddd");就是将String传递给E,用于替代定义中的E
String str=list.get(0);不需要进行类型转换
List<Date> list = new ArrayList<>();// 这里使用菱形语法,支持泛型推导
list.add("abc");// 语法报错,编译时就会进行类型检查
list.add(123);// 语法报错
list.add(new Date());
for (int i = 0; i < list.size(); i++) {
Date temp = list.get(i); // 获取数据不需要类型转换
System.out.println(temp.getYear() + 1900);
}
典型场景
获取两个整数中较大的整数,方法定义为Integer max(Integer a, Integer b){ return a>b?a:b; }
如果需要比较的不是Integer类型,而是Double或是Float类型,那么就需要另外再写max()方法。
引入泛型的目的实际上就是能够在编写max()方法时,不必确定参数a和b的数据类型,而等到调用的时候再来确定这两个参数的数据类型,那么只需要编写一个max()就可以了,这将大大降低程序员编程的工作量。
Comparable接口
Java中有一个系统预定义的接口
public interface Comparable<T> 用于实现规范类的可比较性
int compareTo(T o); 当前类型比较时需要返回一个整数,当当前对象大于参数o时,返回一个正数;如果
等于时则返回0;如果小于时返回一个负数
}
在定义中可以参数Integer类中的方法实现
public static int compare(int x, int y){
return (x<y)?-1:((x==y)?0:1);
}
泛型方法定义
public class MyTest {
public static <T extends Comparable<T>> T max(T t1, T t2) {//extends用于声明传入的T类型必
须是Comparable类型的,必须直接或者间接的实现Comparable接口
return t1.compareTo(t2) > 0 ? t1 : t2;
}
}
Integer kk=MyTest.max(12, 15);
System.out.println(kk);
Double dd=MyTest.max(12.345, 555.34);
System.out.println(dd);
在泛型出现之前,Java的程序员可以采用一种变通的办法:将参数的类型均声明为Object类型。由于Object类是所有类的父类,所以它可以指向任何类对象,但这样做不能保证类型安全。泛型则弥补了Object做法所缺乏的类型安全,也简化了过程,不必显示地在Object与实际操作的数据类型之间进行强制转换。
通过泛型,所有的强制类型转换都是自动和隐式的。因此,泛型扩展了重复使用代码的能力,而且既安全又简单。
泛型的好处
1、可读性,从字面上就可以判断集合中的内容类型;
2 、类型检查,避免插入非法类型。
3 、获取数据时不在需要强制类型转换。
泛型类
所谓泛型类generic class就是具有一个或多个类型参数的类
public class 类型名<泛型名称列表>{ }
public class Student<ID> {
private ID id;
public ID getId() {
return this.id;
}
public void setId(ID id) {
this.id=id;
}
}
声明一个泛型类Generic,使用泛型的语法为<名称>,在类中就可以通过名称直接使用这个类型
Student<String> s1=new Student<>();
// s1.setId(123); 编译报错,因为123不是String类型
s1.setId("123");
Student<Integer> s2=new Student<>();
// s2.setId("123");编译报错,因为"123"不是Integer类型
s2.setId(123);
如果定义了泛型类,但是引用时不声明泛型值系统则识别泛型为Object类型
最后还需要注意:声明一个泛型实例时,传递给形参的实参必须是类类型,而不能使用int或char之类的简单类型。如果不传递类型,则系统默认类型为Object
带两个类型参数的泛型类
如果引用多个类型,可以使用逗号分隔:<S, D>
类型参数名可以使用任意字符串,建议使用有代表意义的单个字符,以便于和普通类型名区分,如:T代表type,有源数据和目的数据就用S/D,子元素类型用E等。当然,你也可以定义为XYZ,甚至xyZ。
泛型中的类型参数严格说明集合中装载的数据类型是什么和可以加入什么类型的数据,记住:Collection 和 Collection 是两个没有转换关系的参数化的类型
集合类中定义范型
范型只能使用引用类型,而不能使用基本类型,即:List是错误的
保持良好的编程风格,尽量使用范型
List list=…
Set set=new HashSet<>();
另外写法
有界类型
public class MyClass{ }表示允许传入给T的类型必须是Number类型的子类型或者Number类型
MyClass mc=new MyClass(); 语法正确,因为Integer是Number的子类型
MyClass mc=new MyClass<>(); 语法错误,因为String不是Number类型
声明泛型可以给泛型添加约束
Java提供了有界类型bounded types。在指定一个类型参数时,可以指定一个上界,声明所有的实际类型都必须是这个超类的直接或间接子类。class classname
public class Oper<T extends Number> {
//不确定个数的参数,进行加减操作。所以参与计算的必须是Number类型
public double add(T...params) {
double res=0;
if(params!=null && params.length>0) {
for(T tmp:params)
res+=tmp.doubleValue();
}
return res;
}
public double sub(T...params) {
double res=0;
if(params!=null && params.length>0) {
for(T tmp:params)
res-=tmp.doubleValue();
}
return res;
}
}
调用泛型类
public class Test1 {
public static void main(String[] args) {
Oper<Float> op=new Oper<>();
double res=op.add(1f,2f,3f,4f,5f,6f,7f);
System.out.println(res);
Oper<Long> op2=new Oper<>();
res=op2.sub(1L,2L,3L,4L,5L,6L,7L);
System.out.println(res);
}
}
接口和类都可以用来做上界。
类充当上界public class Oper
接口充当上界class Stats这里需要注意:针对上界接口使用的关键字仍然是extends而非implements
实现冒泡排序
//要求传入T的类型必须实现了Comparable接口
public class Oper<T extends Comparable<T>> {
public void sort(List<T> list) {
if(list!=null && list.size()>1) {
for(int i=1;i<list.size()-1;i++) {
for(int k=0;k<list.size()-i;k++) {
T c1=list.get(k);
T c2=list.get(k+1);
if(c1.compareTo(c2)>0) { //因为要求T必须实现了Comparable接口,所以T中就有Comparable接口中的方法实现
list.set(k, c2);
list.set(k+1, c1);
}
}
}
}
}
}
Oper<Integer> op = new Oper<>();
List<Integer> list = new ArrayList<>();
Random r=new Random();
for(int i=0;i<10;i++)
list.add(r.nextInt(100));
op.sort(list);
list.forEach(System.out::println);
随机生成字符串
private static final String source="qwertyuiopasdfghjklzxcvbnm1234567890";
public String generateString(int len) {
StringBuilder sb=new StringBuilder();
Random r=new Random();
for(int i=0;i<len;i++)
sb.append(source.charAt(r.nextInt(source.length())));
return sb.toString();
}
一个类型参数可以有多个限界,如class Stats<T extends Comparable & Serializable>。限界类型用&分隔,因为逗号用来分隔类型参数。在多个限界中,可以有多个接口,但最多只能有一个类。如果用一个类作为限界,它必须是限界列表中的第一个。
通配符参数
使用一般用于接收参数
void doSomething(Stats<?> ob) 表示这个参数ob可以是任意的Stats类型。其中?表示一个不确定的类型,它的值会在调用时确定下来。 List<?> list = new ArrayList<String>();
通配符参数的语法使用
.<? extends E>上限通配符,用来限制类型的上限
public class B<T>{
public void pp(List<? extends T> list){} //表示list中元素的类型必须是T类型或者T类型的子类或者T接口的实现类
public void cc(List<T> list){} //要求list中的元素必须是T类型的,假设有实参ArrayList<T子类>传入这个方法则语法报错
}
解决方法:
.<? super E>下限通配符,用来限制类型的下限
public class B<T>{
public void pp(List<? super T> list){}//表示list中元素的类型必须是T类型或者T类父类或者T接口的实现类或者T的父接口实现类
}
小结泛型类
频繁往外读取内容的,适合用上界extends
经常往里插入的,适合用下界Super
泛型方法
所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中(例如public void show(T aa),在使用泛型上没有任何特殊语法要求),也可以定义在普通类中(需要自定义参数类型,那么把泛型参数放在方法上就可以了,就是放在返回值类型之前,例如public void show(T aa)), 静态方法不能访问类的泛型,如果需要泛型只能在方法上使用泛型,例如public static void show(T aa)
在泛型类中包含泛型的方法
在普通类中包含泛型的非静态方法
在普通类中包含泛型的静态方法,注意在返回类型之前的是必须的,不是返回类型
在泛型类中的静态方法上也必须包含,否则编译错误
一个方法如果被声明成泛型方法,那么它将拥有一个或多个类型参数,不过与泛型类不同,这些类型参数只能在它所修饰的泛型方法中使用。
泛型类的继承
泛型类也是可以继承的,任何一个泛型类都可以作为父类或子类
不过泛型类与非泛型类在继承时的主要区别在于:泛型类的子类必须将泛型父类所需要的类型参数,沿着继承链向上传递。这与构造方法参数必须沿着继承链向上传递的方式类似。
子类不是泛型类:需要给父类传递类型常量,如class AA extends A< String>
子类是泛型类:可以给父类传递类型常量,也可以传递类型变量。如class AA3< E> extends
A< E >
如果父类中泛型有约束,在子类声明泛型时就必须有对应的约束
泛型的实现原理
实际上,从虚拟机的角度看,不存在泛型概念。
泛型是运用在编译时期的技术:编译时编译器会按照<类型名>的类型对容器中的元素进行检查,检查不匹配,就编译失败。如果全部检查成功,则编译通过,但,编译通过后产生的.class文件中并没有<类型名>这个标识,即类文件中没有泛型,这就是泛型的擦除。
一句话总结就是:在.java文件运用泛型技术时,编译器在文件编译通过后自动擦除泛型标识。
由于泛型的擦除,类文件中没有泛型机制,同时也没有使用向下类型转换,那么为何运行时无异常?
这是由于泛型的补偿
编译器在擦除泛型后,会自动将类型转换为原定义的"泛型",这样就不必再做向下类型转换了。
泛型的擦除和补偿这两个机制都是编译器内部自动完成的,了解其原理即可。
泛型的局限性
不能使用基本类型
不能使用泛型类异常
不能使用泛型数组
语法正确,这是T[] arr=new T[10]语法报错
不能实例化参数类型对象。例如T ob = new T();