Java~泛型详解

目录

不引入泛型的问题:

什么是泛型?

 使用泛型的定义

 典型场景

 泛型方法的调用

使用泛型的好处 

 泛型的使用

1、泛型类

 带两个类型参数的泛型类

  集合类中定义范型

 有界类型

调用方法:

另外调用方式

泛型方法

泛型类的继承

泛型的擦除 

 泛型的局限性


不引入泛型的问题:

当将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。
因此取出集合元素时需要人为的强制类型转化到具体的目标类型,但是很容易出现ClassCastException异常

那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时
不出现问题,运行时就不会出现java.lang.ClassCastException异常呢?
答案就是使用泛型,可以将运行时的类型检查搬到编译期实现

什么是泛型?

泛型是jdk5引入的类型机制,就是将类型参数化。泛型作为一种安全机制而产生
泛型机制将类型转换时的类型检查从运行时提前到了编译时,使用泛型编写的代码比杂乱的使用object并在需要时再强制类型转换的机制具有更好的可读性和安全性。
泛型在本质上是指类型参数化。所谓类型参数化,是指用来声明数据的类型本身,也是可以改变的,它由实际参数来决定。在一般情况下,实际参数决定了形式参数的值。而类型参数化,则是实际参数的类型决定了形式参数的类型。
在声明List<E>阶段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>();就是将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()就可以了,这将大大降低程序员编程的工作量。

 泛型方法的调用

参与比较的是Integer类型
Integer kk=MyTest.max(12, 15);
System.out.println(kk);
参与比较的是Double类型,不需要重新定义方法
Double dd=MyTest.max(12.345, 555.34);
System.out.println(dd);

在泛型出现之前,Java的程序员可以采用一种变通的办法:将参数的类型均声明为Object类型。

由于Object类是所有类的父类,所以它可以指向任何类对象,但这样做不能保证类型安全。泛型则弥补了Object做法所缺乏的类型安全,也简化了过程,不必显示地在Object与实际操作的数据类型之间进行强制转换。通过泛型,所有的强制类型转换都是自动和隐式的。因此,泛型扩展了重复使用代码的能力,而且既安全又简单

使用泛型的好处 

解决类型安全隐患,泛型的类或接口在取出对象时将不需要再进行向下类型转换,因为存储的时候就是该类型。
泛型的使用让安全问题在编译时就报错而不是运行后抛出异常,这样便于程序员及时准确地发现问题
使用泛型只是带来了附加的类型安全。因为编译器知道将放进集合的类型的更多信息,所以类型检查从执行时挪到了编译时,这会提高可靠性并加快开发速度

package com.yan1;

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

public class Test2 {
	public static void main(String[] args) {
//		List<String> list = new ArrayList<String>();
		List<String> list = new ArrayList<>();
		list.add("abcd");
//		list.add(1234);  编译期语法报错
		for(int i=0;i<list.size();i++) {
			String obj=list.get(i);   //不需要再进行类型转换
			System.out.println(obj);
		}
		
		Double dd=123.756;
		System.out.println(dd.intValue());
		System.out.println(dd.doubleValue());
		Character
	}
}

 泛型的使用

1、泛型类

所谓泛型类generic class就是具有一个或多个类型参数的类

package com.yan2;

import java.util.Date;

/*
 * 所谓泛型类generic class就是具有一个或多个类型参数的类
 */
public class Test1 {
	@SuppressWarnings("all")
	public static void main(String[] args) {
//	A1<Object> aa2=new A1<String>();
		A1<String> aa1 = new A1<>();
		aa1.name = "zhangsan";
//		aa1.name=555;  可以在编译期进行类型检查

		A1<Date> aa3 = new A1<>();
		aa3.setName(new Date());
		System.out.println(aa3.getName().getYear());
		
		
		A1 aa4=new A1();
		aa4.setName("yanjun");
		aa4.setName(123456);
	}
}

class A1<T> { // 在当前类的成员定义中,可以将T当作类型使用
	T name;

	public T getName() {
		return this.name;
	}

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

如果定义了泛型类,但是引用时不声明泛型值,系统则识别泛型为Object类型

A1 aa=new A1();  不会报错,但是有警告信息,不建议这种用法

aa.setName("abc");  正确
aa.setName(123);  正确
name可以传入任意类型的数据,因为系统识别 name为Object类型

class A1中T是类型参数的名称。在创建一个对象时,这个类型名称用作传递给A1的实际类型的占位符。
因此在A1中,每当需要类型参数时,就会用到T。注意T是被括在<>中的。每个被声明的类型参数,都要放在尖括号中。由于Generic使用了类型参数,所以它是一个泛型类,也被称为参数化类型。 
在A1类中使用T定义了一个属性T name。由于T只是一个占位符,所以name的实际类型要由创建对象时的参数传递进来。 
事实是一个数据类型的说明,它可以用来说明任何实例方法中的局部变量、类的成员变量、方法的形式
参数以及方法的返回值。但是类型参数T不能使用在静态方法中。

public class Generic<T> {
    private static T name;//语法报错
    public static T getName(){//语法报错
        return name;
    }
}

最后还需要注意:声明一个泛型实例时,传递给形参的实参必须是类类型,而不能使用int或char之类的简单类型。如果不传递类型,则系统默认类型为Object

 带两个类型参数的泛型类

 如果引用多个类型,可以使用逗号分隔:<S, D>

        类型参数名可以使用任意字符串,建议使用有代表意义的单个字符,以便于和普通类型名区分,如:T代表type,

        有原数据和目的数据就用S/D,子元素类型用E等。当然,你也可以定义为XYZ,甚至xyZ。
   
        泛型是JDK1.5的所有新特性中最难深入掌握的部分,没有使用泛型时,只要是对象,不管是什么类型的对象,都可以存储进同一个集合中

List list=new ArrayList(); 
list.add("123"); 
list.add(456); 
list.add(new Date());
因为系统识别的类型为Object

使用泛型集合,可以将一个集合中的元素限定为一个特定类型,这样集合中就只能存储同一类型的对象,这样更安全;并
且当从集合中获取一个对象时,编译器也知道这个对象的类型,不需要对对象进行强制类型转换,这样更方便。

List<String>list=new ArrayList<>(); 第2个类型可以不写,JDK1.7引入的泛型推导
list.add(“123”);正确
list.add(456);错误,类型不合法

在JDK1.5之后,你还可以按原来的方式将各种不同类型的数据放到同一个集合中,但是编译时会报一个unChecked警告

可以使用注解避免警告信息
    @SuppressWarnings("rawtypes")
    A1 aa4=new A1();
    去除所有的警告信息  @SuppressWarnings("all")

泛型中的类型参数严格说明集合中装载的数据类型是什么和可以加入什么类型的数据,

记住:Collection<String>和Collectin<Object>是两个没有转换关系的参数化的类型

  集合类中定义范型

List<String> list  = new ArrayList<String>();
Map<String,Object> map = …
List<Map<String,Object>> list …

范型只能使用引用类型,而不能使用基本类型,即:`List<int>`是错误的

最佳实践:保持良好的编程风格,尽量使用范型

 有界类型

public class MyClass<T extends Number>{  }  含义是要求传入T的类型必须是Number或者Number的子类型
在实际应用中可能会需要对传递给类型参数的类型加以限制。比如需要创建一个泛型类,它包含了一个求数组平均值的方法。这个数组的类型可以是整型、浮点型,但显然不能是字符串类型或是其他非数值类型。

Java提供了有界类型bounded types。在指定一个类型参数时,可以指定一个上界,声明所有的实际类型都必须
是这个超类的直接或间接子类。class classname <T extends superclass>要求传入的泛型类型必须是Number抽象类的子类 

 public class Generic<T extends Number>{
       public double sum(T... params) {
        	double res=0;
        	for(int i=0;i<params.length;i++)
        		res+=params[i].doubleValue(); //这里是Number中定义的方法
        	return res;
    	}
	}
	注意:这里的extends不表示继承,只是说明传入的类型必须是Number或者Number的子类型

调用方法:

Generic<Long> g=new Generic<>():
double sum=g.sum(1L,2L,3L,4L,5L,6L);
System.out.println(sum);

因为Long是Number的子类,但是换一种类型String则会报错

Generic<String> g=new Generic<>(); //报错的原因是String不是Number的子类型

另外调用方式

Generic<Number> g=new Generic<>();
double sum=g.sum(1,2,3,4,5);  //写法正确,因为Integer(1)也是Number类型的

接口和类都可以用来做上界。class Stats<T extends Comparable> 。
这里需要注意:针对上界接口使用的关键字仍然是extends而非implements。一个类型参数
可以有多个限界,如class Stats<T extends Comparable & Serializable>。限界类型用&分隔,因为逗号用来分隔类型参数。在多个限界中,可以有多个接口,但最多只能有一个类。如果用一个类作为限界,它必须是限界列表中的第一个

public class Test2 {
	public static void main(String[] args) {
//		MyClass<String> mc = null;
//		MyClass<Date> mc2 = null;
		MyClass<Integer> mc1 = new MyClass<>();
		MyClass<Double> mc2 = new MyClass<>();

		MyClass<Number> mc3 = new MyClass<>();
		mc3.add(123); // Integer是Number的子类型
		mc3.add(12.345);

		Number kk = mc3.get();

		A2<B2> aa = new A2<>();

		D2<G2, T2> dd = new D2<>();
	}
}

class MyClass<T extends Number> {
	private T id;

	public void add(T t) {
		this.id = t;
	}

	public T get() {
		return id;
	}
}

class A2<T extends Comparable<T> & Serializable> {
}

class B2 implements Comparable<B2>, Serializable {
	public int compareTo(B2 o) {
		return 0;
	}
}

//如果上限类型为类,不是接口,则只能有一个;如果有多个,则至少一个是接口
class C2<T extends Date & Serializable> {

}

class E2 {
}

class F2 {
}

class G2 extends E2 {
}

class H2 extends F2 {
}
class T2{}
class D2<T extends E2, F2> {
	T id;
	String name;
}

class Y2<T extends E2,String>{
	
}

泛型方法

所谓泛型方法,就是带有类型参数的方法,它既可以定义在泛型类中(例如public void show(T aa),在使用泛型上没有任何特殊语法要求),也可以定义在普通类中(需要自定义参数类型,那么把泛型参数放在方法上就可以了,就是放在返回值类型之前,例如public <T> void show(T aa)), 
静态方法不能访问类的泛型,如果需要泛型只能在方法上使用泛型,例如
public static <T> void show(T aa)

在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。 

package com.yan4;

import java.util.Date;
/*
 * 可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,
 * 编译器适当地处理每一个方法调用。
 */
public class Test1 {

}

class A1<T, ID, BB, CC> {
	T id;
	ID name;
	BB age;
	CC birth;

	public void pp(T t) {

	}
}

class B1 {
	public <T> void pp(T t) {
		System.out.println(t.toString());
		System.out.println(t.hashCode());
		System.out.println(t.getClass());
	}

	public <T extends Comparable<? extends T>,D> void bb(T t,D d) {
		t.compareTo(null);
	}

	public <T> T cc(T t) {
		return null;
	}

	public static void main(String[] args) {
		B1 bb = new B1();
		bb.pp(123);
		bb.pp("abc");

		bb.bb(new Date(),123);
	}
}
package com.yan4;

import java.util.Date;

public class Test2 {
	public static void main(String[] args) {
		B2 bb = new B2();
		bb.cc(123);
		bb.cc("tttt");

		B2<String> b2 = new B2();
		b2.cc("123");// 如果123则报错

		b2.bb(123); // 静态方法上的<T>和类上的<T>无关

		int kk = b2.dd(123);// dd方法上有<T>声明,则和类上的<T>无关
		String ss = b2.dd("abcd");
		Date dd = b2.dd(new Date());
	}
}

class A2 {
	public static <T> void pp(T t) {
	}
}

class B2<T> {
	public void cc(T t) {
	}

	public static <T> T bb(T t) {
		return null;
	}

	public <T> T dd(T t) {
		return null;
	}
}

泛型类的继承

泛型类也是可以继承的,任何一个泛型类都可以作为父类或子类。不过泛型类与非泛型类在继承时的主要区别在于:
泛型类的子类必须将泛型父类所需要的类型参数,沿着继承链向上传递。这与构造方法参数必须沿着继承链向上传递的方式类似。
子类不是泛型类:需要给父类传递类型常量,如class AA extends A<String>,如果不设置
则默认class Aa extends A<Object>
子类是泛型类:可以给父类传递类型常量,也可以传递类型变量。如class AA3<E> extends A<E> 
如果父类中泛型有约束
public class A1<T extends Serializable> {} 
public class B1<D extends Serializable> extends A1<D>{}  //声明子类型时在子类上声明约束

可以写成public class B1<D extends Serializable> extends A1{},此时A1中T就是Object类型的

泛型的擦除 

实际上,从虚拟机的角度看,不存在泛型概念。
泛型是运用在编译时期的技术:编译时编译器会按照<类型名>的类型对容器中的元素进行检查,检查不匹配,就编译失败。如果全部检查成功,则编译通过,但编译通过后产生的.class文件中还有<类型名>
这个标识,即字节码的类文件中有泛型,但是运行时并没有泛型这就是泛型的擦除。
可以通过使用javap反编译查看字节码文件,可以看到其中包含泛型
一句话总结就是:在.java文件运用泛型技术时,编译器在文件编译通过后运行时自动擦除泛型标识。
如果需要使用泛型的类型,则需要通过反射机制进行保存

abstract class A1<TF> {
			private Class<TF> clz; // 就是T的具体类型
		
			// 在构造器中获取T的具体类型
			public A1() {
				ParameterizedType c = (ParameterizedType) this.getClass().getGenericSuperclass(); // 获取到父类型的定义com.yan6.A1<T>
				clz=(Class)(c.getActualTypeArguments()[0]);  //获取真实的类型参数  T
			}

 由于泛型的擦除,运行时并没有泛型机制,同时也没有使用向下类型转换,那么为何运行时无异常?
 答:这是由于泛型的补偿

List<String> list=new ArrayList<>();
  for(String tmp:list) System.out.println(tmp.length());  //泛型的补偿
  编译器在擦除泛型后,会自动将类型转换为原定义的泛型,这样就不必再做向下类型转换了。

for(Object tmp:list){
 		if(tmp instanceof String){
 			System.out.println(((String)tmp).length());
 		}

泛型的擦除和补偿这两个机制都是编译器内部自动完成的。可以通过反射获取类型参数

 泛型的局限性

  1. 不能使用基本类型
  2. 不能使用泛型类异常
  3. 不能使用泛型数组
  4. 不能实例化参数类型对象。【例如T ob = new T();】
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值