Java程序设计--泛型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014621467/article/details/81906097

目录

 1.为什么引入泛型

2.什么是泛型

3. 泛型的使用

4. 泛型的优势

5. 泛型实现机制-类型擦除

6.VS C++模板


 1.为什么引入泛型

1.重复逻辑的例子(该示例来自于博客:https://blog.csdn.net/qq_27093465/article/details/73229016

public class IntegerPoint {
	private Integer x; // 表示X坐标
	private Integer y; // 表示Y坐标

	public void setX(Integer x) {
		this.x = x;
	}

	public void setY(Integer y) {
		this.y = y;
	}

	public Integer getX() {
		return this.x;
	}

	public Integer getY() {
		return this.y;
	}
}


public class FloatPoint {
	private Float x; // 表示X坐标
	private Float y; // 表示Y坐标

	public void setX(Float x) {
		this.x = x;
	}

	public void setY(Float y) {
		this.y = y;
	}

	public Float getX() {
		return this.x;
	}

	public Float getY() {
		return this.y;
	}
}

 为了减少重复性代码,尝试使用继承来实现该功能,从而提高代码复用率,减少重复代码:

public class ObjectPoint {
	private Object x;
	private Object y;

	public void setX(Object x) {
		this.x = x;
	}

	public void setY(Object y) {
		this.y = y;
	}

	public Object getX() {
		return this.x;
	}

	public Object getY() {
		return this.y;
	}
}


public class IntegerPoint extends ObjectPoint{

}

public class FloatPoint extends ObjectPoint {
	
}

由于IntegerPoint类和FloatPoint类都继承ObjectPoint类,继承其get和set方法,可以避免在两个类中出现重复代码。但是,这样设计有一个问题:

public static void main(String[] args) {
		
	ObjectPoint point1 = new IntegerPoint();
	point1.setX(11.11);
	point1.setY(22.22);
		
	ObjectPoint point2 = new FloatPoint();
	point2.setX(new Integer(11));
	point2.setY(new Integer(22));
		
	Integer x = (Integer)point1.getX() + (Integer)point2.getX();
	System.out.println(x);
		
}

这种写法,通过强制类型转换,在子类之间进行了转换,由于set方法的参数是Object类型,是父类型,在get时获得的也是object类型,编译器不知道其引用的究竟是哪一个子类型,因此在进行强制类型转换的时候,不会编译错误。但在运行时就会报java.lang.ClassCastException错,从而运行失败。

扫描二维码关注公众号,回复: 3062156 查看本文章

2.集合的例子

public static void main(String[] args) { 
     List list = new ArrayList(); 
     list.add("zhangSan"); 
     list.add("男"); 
     list.add(100); 
     for (int i = 0; i < list.size(); i++) { 
            String str = (String) list.get(i); 
            System.out.println("str:" + str); 
     } 
} 

从上面这个集合的例子,可以看到:ArrayList中可以存放各种类型的对象,因此放入两个字符串和一个整数是可以编译通过且正常运行的。但是当对集合中的每个对象进行操作时,就容易出现对整型对象使用字符串类型的方法的情形,导致运行时报java.lang.ClassCastException错,运行失败。

 

2.什么是泛型

泛型,即“参数化类型”。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。该解释来源于博客:https://www.cnblogs.com/ljxe/p/5521840.html

泛型类:具有一个或多个泛型变量的类。在类的声明处加入<T>,该类就成为一个泛型类。其中的字母可以任意取,但一般情况用T、U。(E表示Element、K-V表示键值对。约定俗成,非强制规定)

public class Pair<T> {

	private T first;
	private T second;
	
	public Pair() {
		first = null;
		second = null;
	}

	public Pair(T first, T second) {
		this.first = first;
		this.second = second;
	}

	public T getFirst() {
		return first;
	}

	public void setFirst(T first) {
		this.first = first;
	}

	public T getSecond() {
		return second;
	}

	public void setSecond(T second) {
		this.second = second;
	}
}

 泛型方法:可以定义在普通类中,也可以定义在泛型类中

class ArrayAlg {
      public static <T> T getMiddle(T... a) {
            return a[a.length / 2];
      }
}

// 调用方法
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");

类型变量的限定:使用extends关键字限定泛型类型,多个限定通过 & 分隔。可以通过接口限定类型,也可以通过类限定。类需放在所有接口的最前面。

public static <T extends Comparable & Serializable> T min(T[] a) {
        // T 的实际类型必须同时实现了Comparable和Serializable接口
}

3. 泛型的使用

  • 泛型类 

通过泛型类的方式实现坐标的例子:

public class Point<T> {
	private T x;
	private T y;

	public void setX(T x) {// 作为参数
		this.x = x;
	}

	public void setY(T y) {
		this.y = y;
	}

	public T getX() {// 作为返回值
		return this.x;
	}

	public T getY() {
		return this.y;
	}
}

 通过泛型定义了坐标类型,在实例化整型坐标和浮点型坐标时指定类型,中间对对象赋值操作不正确时编译器检查会不通过。

 

 在ArrayList声明时指定String类型,在通过add方法向链表中添加元素时,编译器会对元素类型进行检查,非String类型的元素不能加入到链表中,编译不通过。同样,通过get方法访问链表元素时,也无需进行强制类型转换。

在Java1.5时,List、Map等集合类型引入了泛型机制,但为了向下兼容,保留了原始类型,即可以不指定泛型类型。

  • 泛型接口

泛型接口与泛型类基本相同,例如:Iterator接口

public interface Iterator<E> {

    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

}
  • 泛型方法

 泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。泛型方法与泛型类是相对独立的。

方法的声明由:修饰符 返回值类型 方法名 参数列表组成,而泛型方法在修饰符和返回值类型之间多了一部分<T>,可以理解为泛型方法的独特声明。因此,泛型类中定义的成员变量不一定就是泛型方法,泛型方法同样也不一定要在泛型类中 声明。

静态方法:静态方法不能直接访问泛型类的泛型变量

 如图所示,泛型类中定义静态方法,该静态方法参数为泛型类的类型变量,编译不通过。如果静态方法需要使用泛型变量,则该静态方法需要声明为泛型方法

4. 泛型的优势

  1. 可读性:泛型中,对于具体的类使用,可以清晰看出该类的类型

  2. 安全性:编译器对其进行检查,避免或减少运行时出现的强制类型转换的错误

  3. 可重用性:类型的参数化,编写的代码可以被很多不同类型的对象所重用,避免重复逻辑或重复代码,提高代码质量,也提高代码可读性

5. 泛型实现机制-类型擦除

  •  虚拟机没有泛型类型对象,所有对象都属于普通类
public static void main(String[] args) {
	List<String> names = new ArrayList<>();
	List<Integer> ages = new ArrayList<>();
	List<Number> weights = new ArrayList<>();
		
	System.out.println("names class:" + names.getClass());
	System.out.println("ages class:" + ages.getClass());
	System.out.println("weights class:" + weights.getClass());
}

 运行结果为: 

names class:class java.util.ArrayList
ages class:class java.util.ArrayList
weights class:class java.util.ArrayList

可以看出,在虚拟机中,不区分ArrayList的参数类型,统统为ArrayList类型。因此,泛型仅仅只在编译阶段有效,编译通过之后,虚拟机会将类型参数擦除,也就是说泛型信息不会进入运行时阶段。(强制类型转换也是存在的)

上述代码中,Number是Integer的父类,试想:List<Number> numberList1 = ages; 该语句是否成立呢?

答案是否定的,编译不通过:Type mismatch: cannot convert from List<Integer> to List<Number>。 虽然Integer是Number的子类,但List<Integer>和List<Number>之间没有关系。(如果希望二者之间有逻辑上的继承关系,可以使用通配符?帮助实现。如:List< ? extends Number > numberList2 = ages)

public class Test {

	public static void main(String[] args) {
		List<String> names = new ArrayList<>();
		List<Integer> ages = new ArrayList<>();
		List<Number> weights = new ArrayList<>();
		
//		List<Number> numberList1 = ages;   //编译不通过,报错
		List< ? extends Number > numberList2 = ages;
		
		Test test = new Test();
		ages.add(new Integer(11));
//		System.out.println(test.fun(ages));   //编译不通过,报错
		System.out.println(test.fun1(ages));
	}

	public Number fun(List<Number> list) {
		return list.get(0);
	}
	
	public Number fun1(List<? extends Number> list) {
		return list.get(0);
	}
}
  • 类型擦除:擦除类型变量,替换为限定类型
  1. 使用第一个限定的类型变量来替换(可以通过接口限定类型,也可以通过类限定。类需放在所有接口的最前面。限定顺序是重要的)
    public class Pair<T extends Comparable & Serializable> {
    
    	private T first;
    
    	public T getFirst() {
    		return first;
    	}
    
    	public void setFirst(T first) {
    		this.first = first;
    	}
    }
    
    //类型擦除后
    
    public class Pair {
    
    	private Comparable first;
    
    	public Comparable getFirst() {
    		return first;
    	}
    
    	public void setFirst(Comparable first) {
    		this.first = first;
    	}
    }
  2. 如果没有限定,则使用Object类型替换

    public class Pair<T> {
    
    	private T first;
    
    	public T getFirst() {
    		return first;
    	}
    
    	public void setFirst(T first) {
    		this.first = first;
    	}
    }
    
    //类型擦除后
    
    public class Pair {
    
    	private Object first;
    
    	public Object getFirst() {
    		return first;
    	}
    
    	public void setFirst(Object first) {
    		this.first = first;
    	}
    }
  • 翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器自动进行强制类型转换

如下图: 链表在声明时指定String类型,add时只能添加String类型对象,取出元素时不需要进行强制类型转换,编译器自动插入强制类型转换

  •  翻译泛型方法
public static <T extends Comparable> T min(T[] a)

//擦除类型后

public static Comparable min(Comparable[] a)


class DateInterval extends Pair<LocalDate>
{
    public void setFirst(LocalDate first) {
        ... ...
    }
}

//擦除类型后

class DateInterval extends Pair
{
    public void setFirst(LocalDate first) {
        ...  ... 
    }
}

//由于继承Pair,DateInterval还存在一个 public void setFirst(Object first) 方法

 当定义如下语句时:

DateInterval interval = new DateInterval(...);

Pair<LocalDate> pair = interval;      //子类对象即父类对象, OK

pair.setFirst(aDate);   

由于pair引用的是DateInterval类型的对象,因此最为正确的是调用DateInterval类的setFirst方法。

但由于类型擦除了,父类中方法变成了setFirst(Object object),DateInterval中的setFirst(LocalDate first)方法并没有覆盖掉父类中的方法,所以DateInterval同时存在两个setFirst方法。

pair对象声明为Pair<LocalDate>类型的,该类型只有setFirst(Object object)方法。虚拟机通过pair引用的DateInterval类型的对象去调用这个方法,因此会调用DateInterval类的setFirst(Object object)方法,这个方法是编译器生成的一个“桥方法”:

class DateInterval {
    public void setFirst(Object object) {
        setFirst((Date)object);
    }
}

6.VS C++模板

  • 关键字:Java泛型没有特殊关键字标识,C++使用Template关键字标识

  • C++模板可以使用基本数据类型,Java 泛型不能接受基本类型作为类型参数――它只能接受引用类型。这意味着可以定义 List<Integer>,但是不可以定义 List<int>

  • C++

  • 在Java中,不管类型参数是什么,泛型类的所有实例都是同一类型类型参数会在运行时被擦除。而C++中,参数类型不同,实例类型也不同。在c++中存在为每个模板的实例化产生不同的类型,这一现象被称为“模板代码膨胀”

  • Java中,泛型类的类型参数变量 T 不能用于静态方法和静态变量,因为他们会被MyClass<Foo>和MyClass<Bar>共享。但在C++中,这些类是不同的,类型参数可以用于静态方法和静态变量。

  • 在java中,尖括号通常放在方法名前,而c++则是放在方法名后,c++的方式容易产生歧义,例如g(f<a,b>(c)),这个则有两种解释,一种是f的泛型调用,c为参数,a,b为泛型参数。另一种解释,则是,g调用,两个bool类型的参数。

猜你喜欢

转载自blog.csdn.net/u014621467/article/details/81906097