Java通识基础26(泛型)
浅层了解——泛型是定义集合类时所需要的
用来固定集合类的类型
- 集合类在初始化的时候默认的的类型是
Object
,调用时总是需要用到强制类型转换 - 集合类在初始化的时候默认的的类型是
Object
,所以任意元素都可以添加到集合类中
在集合类使用泛型和菱形语法的简例:
import java.util.ArrayList;
import java.util.List;
public class NoGeneric {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("JS");
for (Object st:list){
System.out.println(st);
}
System.out.println(list);
}
}
自定义的泛型类
在自定义的类中定义泛型
根据该类创建对象或者调用对象的方法时可以可以根据泛型传入实际类型
这样的优点便是不仅让传入的值可以借助泛型动态改变,而传入的类型也可以动态改变
- 方法:程序为方法定义几个形参,调用方法就会传入几个实参
- 泛型:程序为类或者方法传入几个泛型,使用雷和方法的时候后就会传入几个实际类型
public class Apple<T,E> {
private T name;
private E weight;
//在自定义类中定义了泛型,就可将该类体中使用这两个泛型
//好处是变量的指定不会固化
public void setName(T name) {
this.name = name;
}
public T getName() {
return name;
}
public void setWeight(E weight) {
this.weight = weight;
}
public E getWeight() {
return weight;
}
}
ublic class AppleTest {
public static void main(String[] args) {
Apple<String, Float> app1 = new Apple<>();
app1.setName("Java");
System.out.println(app1.getName());
Apple<Integer, String> app2 = new Apple<>();
app2.setName(233);
System.out.println(app2.getName());
Apple app3=new Apple();
app3.setName("Java"); //Object类型
String s=(String)app3.getName();
System.out.println(s);
}
}
使用泛型派生子类
子类可以完全继承父类的泛型
class BlackApple<T,E> extends Apple<T,E>{
}
也可以相应的为父类泛型传入实际类型
class GreenApple extends Apple<String,Float>{
}
或者是创建的时候自己重新定义泛型
class RedApple<A> extends Apple{
}
原始类型
虽然程序定义了泛型类,但当我们练尖括号都不加的时候,系统就会默认定义原始类型
——其目的是与泛型出现之前的代码保持兼容
缺点
- 只要使用了原始类型,Java就会警告
- 原始类型会被编译器当做
Object
来处理,此后使用需要用到强制类型转换
泛型不支持形变!——并不存在的泛型类
List<String>、List<Integer>
看上去很像List
的子接口
但是他们没有真正的类文件,使用的类已仍然是List
类型通配符
当我们使用泛型类,程序应该为泛型形参传入实际的类型参数,但由于不确定应该传入哪种类型,所以就用?
来当做通配符
import java.util.List;
public class WildcardTest {
//List<?>表示需要为此处的泛型传入实际的类型,但不确定使用哪种类型,所以使用了通配符
public static void foreach(List<?> list){
for (Object o : list) {
System.out.println(o);
}
}
public static void main(String[] args) {
List<Integer> inList= List.of(2, 3, 6);
WildcardTest.foreach(inList);
List<String> strList= List.of("fk","cao","cyka");
WildcardTest.foreach(strList);
}
}
- 使用了通配符之后,通配符本身可以代表任何类型
通配符存在的限制
通配符虽然可以代表任何类型,但Java永远无法确定通配符到底代表了什么实际类型
- 如果你尝试向
List<?>
添加List<Integer>
,Java有可能会让List<?>
引用List<Float>
,也有可能是List<Double>
,所以,程序会报错!
所以,通配符不能添加元素(Java也不清楚你添加的是什么类型的元素),只能删除元素
List<?>
与List<Object>
的区别
前者可以被任何类型代替,当你有一个方法不确定使用的是哪一个实际类型时就可以使用通配符
List<Object>
却固定对标Object
,根据前题并不存在的泛型类可知我们无法用List<Object>
代替任何其他类型,所以使用的时候避免不了强制类型转换
List<?>
与List
(原始类型)的区别
List<?>
是保留泛型信息的,调用的时候可以存在类型上的约束List
不保留泛型信息,集合被赋值给List
变量之后,他们的泛型信息都会被擦除!
类型通配符的上限
作用:
- 仍然可以保留类型通配符的优势
- 可以保证从集合取出的元素是某个类型或其任意的子类
语法:
List<? extands 上限>
规则:
- 带上限的类型通配符的集合,同样只能取出元素,而且被取出的元素总是被当成“上限”类型来处理
List<类型>
(只要尖括号中的类型是上限或其子类),那么List<类型>
就可以被当成List<? extands 上限>
import java.util.List;
public class UpperLimit {
public static void main(String[] args) {
//定义一个List<Integer>类型
List<Integer>intList=List.of(2,3,6);
//List<? extands Number>为通配符上限
//类似于之前的通配符,它可以去除元素且取出的元素作为Number来处理
//只要尖括号中的类型是Number或者其子类
List<? extends Number>numList=intList;
//注意该集合只能取出元素,并且其所有元素都被扩展为Number类型
Number i=numList.get(0);
System.out.println(i);
List<Double> doublelist=List.of(2.2,2.3);
numList=doublelist;
Number q=numList.get(1);
System.out.println(q);
}
}
从上面的例子不难看出,通配符上限的使用是为了给固有的泛型进行上向转型(向上扩展)
比如numList
这一带上限的通配符从Integer
或是Double
泛型转换为了Number
且固定为了Number
注意:带上限的通配符无法添加元素,因为我们不清楚该通配符的集合元素代表的是“上限”类型的哪一个子类,比如尝试在numList
中添加Double
元素,其集合元素可能是Integer
,因此会报错
所以,同样的,带上限的通配符也只能取出元素
综上List<?>
只是List<? extands 上限>
的特例
综上List<?>
等价于List<? extands Object>
举例
public static void listSum(List<Integer> list){
int sum=0;
for(Integer i:list){
sum+=i;
}
}
局限于该方法只能计算类型是整形的元素的之和
改进
public static Double listSum(List<? extends Number> list){
double sum=0;
for(Number i:list){
sum+=i.doubleValue();
}
return sum;
}
通过形参可得出此时该方法可以适用于泛型为Number
的集合
类型通配符在自定义泛型的运用
import java.util.List;
public class UpperLimit {
public static void main(String[] args) {
Item<String> stringItem=new Item<>("java");
stringItem.printInfo("Juan");
Item<? extends Number> numItem;
numItem=new Item<Integer>(233);
numItem.getInfo();
//我们只知道numItem的泛型是Number以及其子类,但无法确定具体是哪一个子类
//System.out.println(numItem.printInfo(233));
//printInfo方法的参数是T,此时T代表的是上限为Number类型的通配符,但我们不清楚具体是哪一种类型,所以此对象的该方法永远无法被调用!
}
class Item<T> {
private T info;
public Item(T info) {
this.info = info;
}
public void getInfo(){
System.out.println(info);
}
public void printInfo(T t) {
System.out.println("参数t为" + t);
}
}
**重点总结1:
对于带上限的通配符的泛型类的实例而言
- 只能取出元素——也就是说只能调用返回值为泛型的方法
- 不能添加元素——不能调用参数为泛型的方法
——协变只能出不能进
类型通配符的下限
语法:
List<? super 下限>
规则
- 与类型通配符的上限相反——只能添加元素,不能取出元素
- 添加的元素必须是下限或者是下限的子类
- 如果类A是类B的父类,那么
List<A>
就相当于List<? super B>
的子类(此时只要求集合的泛型是B的父类具体是哪一个父类不确定)——这个概念被称作泛型逆变
**重点总结2:
对于带上限的通配符的泛型类的实例而言
- 只能添加元素——也就是说只能调用参数为泛型的方法
- 不能取出元素——不能调用返回值为泛型的方法——除非把返回值当做
Objext
处理
——逆变只能出不能进
泛型(形参)的上限
运用场景——
例如对于class A<T>
意味着泛型T
只需要是Object
的子类,T
可以是任意的类型
在某些时候,程序对于泛型T
的要求只是某个类的子类,此时可以通过泛型上限来实现
语法猜也可以猜到:
class A<T extands 上限>
举例:
public class Users1 <T extends Number> {
private T weight;
public void setWeight(T weight) {
this.weight = weight;
}
public T getWeight() {
return weight;
}
}
class UserTest{
public static void main(String[] args) {
Users1<Integer> u1=new Users1<>();
u1.setWeight(3);
System.out.println(u1.getWeight());
Users1<Double> u2=new Users1<>();
u2.setWeight(1.43);
System.out.println(u2.getWeight());
}
}
泛型的上限可以是多个类型
多个泛型上限时,只能有一个类,可以有多个接口(Java语言的单继承性)
import java.io.Serializable;
//此处限定T必须是Number的子类,并且是Serializable和Comparable的接口
public class Foo <T extends Number & Serializable & Comparable>{
}
泛型方法
定义方法的时候,可以在修饰符与返回值类型之间用尖括号来定义额外的类型形参(泛型)
泛型类和泛型方法的本质区别——
如果在类中定义了泛型,那么该类中所有的方法都可以使用该泛型
如果只是在方法中定义了泛型,那么只能在该方法中使用该泛型
所以本质上相同,仅仅是作用域不同
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
public class Utils {
//将数组的元素复制到集合中
public static<T> void copyArraytoCollection(T[] from, Collection<T> to){
Collections.addAll(to, from);
}
public static void main(String[] args) {
String[] strA=new String[]{"java","love","gtn"};
Double[] strD=new Double[]{2.1,2.22,23.3};
Collection<Object> M1=new ArrayList<>();
copyArraytoCollection(strA,M1);
copyArraytoCollection(strD,M1);
System.out.println(M1);
}
}
同理,对构造器使用泛型的时候,同样无需显示指定的泛型类型
public class Utils<E> {
public <T> Utils(T info,E into){
System.out.println(info);
}
}
class UnitTest{
public static void main(String[] args) {
Utils u1=new Utils(11,"spt");
Utils u2=new Utils("lsr",22);
//声明时到指定类型的泛型
Utils <String>u3=new <Double>Utils("lsr",22.1);
}
}
注意:此处显示指定的构造器类型,就不能使用菱形语法
总结——要么都显示指定,要么都不显示指定
泛型方法重载
由于泛型方法中的形参类型是动态改变的,而Java的方法重载是根据方法传入的形参类型区分的。
因此可以出现一种情况——Java程序中的两个形参列表看上去不同,但实际上是完全一样的
import java.util.Collection;
public class Mine {
public static <T> void copy(Collection<T> from, Collection<? super T> to){
to.addAll(from);
}
public static <T> void copy(Collection<? super T> from, Collection<? super T> to){
to.addAll(from);
}
}
此时Java编译器无法准确区分两个方法,编译时会报错!
注意:在方法中引入泛型之后,方法的签名会变得更加灵活,要区分何时是方法重载
泛型擦除
存在目的是与早起程序保持兼容
当程序将一个带泛型信息的变量赋值给不带泛型信息的变量时,所有的泛型信息都会被丢失——泛型擦除
import java.util.ArrayList;
import java.util.List;
public class Erase {
public static void main(String[] args) {
List<Integer> integerList=new ArrayList<>();
integerList.add(2);
integerList.add(14);
System.out.println(integerList.get(0));
//开始擦除
List list=integerList;
list.add("wtf");
//list擦除了所有的泛型信息,所有的元素都是Object
System.out.println(list);
Object i=list.get(0);
System.out.println(i);
}
}