JavaSE高级程序设计总结、训练——泛型程序设计1
一、泛型类、泛型方法、泛型接口
1.1、示例程序:
/**
* funcution : 泛型类、泛型方法、泛型接口
* @author jiangzl
*/
public class MyGeneric1 implements AddValue<Interval<Integer>> {
public static <T> Interval<T> getReverse(Interval<T> interval){
return new Interval<>(interval.getUpper(), interval.getLower());
}
public static void main(String[] args) {
Interval<Integer> interval = new Interval<>(1, 2);
System.out.println(interval);
Interval<Integer> integerInterval = new Interval<>(3, 4);
System.out.println(getReverse(integerInterval));
System.out.println((new MyGeneric1()).add(interval, integerInterval));
}
@Override
public Interval<Integer> add(Interval<Integer> e1, Interval<Integer> e2) {
return new Interval<>(e1.getLower() + e2.getLower(), e1.getUpper() + e2.getUpper());
}
}
class Interval<T> {
private T lower;
private T upper;
public Interval(T lower, T upper){
this.lower = lower;
this.upper = upper;
}
public T getLower(){
return lower;
}
public T getUpper(){
return upper;
}
@Override
public String toString(){
return new String("lower = " + lower + ", upper = " + upper);
}
}
interface AddValue<T> {
/**
* 实现两个 T 类型的对象相加
* @param e1
* @param e2
* @return e1 + e2
*/
public T add(T e1, T e2);
}
看看运行的结果:
1.2、正题:泛型类
一个类,里面有很多不同数据类型的成员变量,也有很多不同返回值类型的方法,这些方法的参数的类型也各不相同。不过我们可以设计一种泛型,让一个类接受不同的数据类型,可以根据用户的需要,针对不同的数据类型完成功能!
1.2.1、什么时候用泛型类?
个人理解,针对成员变量数据类型一致(或数据类型种类不多的)类的设计,可以考虑泛型。因为它们往往对不同的数据类型都使用着相同的表达形式(构造)、处理方式(方法)。
举个实例,看上面1.1节讲述的代码:
class Interval<T> {
private T lower;
private T upper;
public Interval(T lower, T upper){
this.lower = lower;
this.upper = upper;
}
public T getLower(){
return lower;
}
public T getUpper(){
return upper;
}
@Override
public String toString(){
return new String("lower = " + lower + ", upper = " + upper);
}
}
这个类表示的是《区间》这么一个概念,这个概念需要的成员变量有左端点、右端点(上下界),同一种区间的表示里,上下界的数据类型是一致的,区间的处理方法(区间交、并、补等运算)是一致的。它们除了数据类型不同,其他的东西都一致,所以在这种类的设计里,我们往往采用《泛型程序设计》!
1.2.2、泛型类的声明规范
这个其实非常简单,只需要在类名后面加上尖括号,然后里面写泛型的名称,一般用T来表示。这个T的意思就是Template,因为Java里面没有template关键字,所以只需要在类名后面紧跟尖括号,然后注明T即可。
声明了这个泛型类之后,就相当于在类的声明范围内,声明了T这个数据类型。所以后面的数据成员、方法的返回值、参数等,都可以用T来声明。
1.2.3、泛型类的实例化
在实例化之前,它们称之为泛型,实例化之后,就是具体的类了!所以在实例化的时候,我们需要在尖括号内填写实例的数据类型,比如上述的代码中:
Interval<Integer> interval = new Interval<>(1, 2);
这里和一般类的实例化差不多,只是在类名后面加上了尖括号,并且尖括号内要写上具体的数据类型。并且在new之后的类名的后面加上尖括号。好像是从jdk1.7开始,这个尖括号就可以省略了,因为可以从前面的尖括号得到具体的实例的数据类型。然后小括号内写啥就根据你的构造方法了!
然后这里有个值得注意的地方:
大家看到没有,我们实例化泛型的时候,用的是Integer,而不是int,那么可不可以用int然后自动装箱呢?答案是不可以的!泛型只接受类,不接受某拆箱后的数据类型,所以只可以用包装类、自定义类等《类属性》的类名嗷!~~
1.3、正题:泛型方法
1.3.1、设计一个泛型方法
所谓泛型方法,就是具有泛型参数的方法,它们也可以返回一个泛型类的对象。比如之前这个例子:
public static <T> Interval<T> getReverse(Interval<T> interval){
return new Interval<>(interval.getUpper(), interval.getLower());
}
这里有个值得注意的地方:
大家可以看到,在返回值类型的前面,修饰符的后面,有一个令人费解的:< T > 它是什么滴干活??
它叫做类型参数,目的是告诉整个函数有这么个类型,然后你才可以使用它。( 个人理解,有不对的地方请评论、私信指正,谢谢! )
然后此处函数的参数也是泛型,泛型方法具体化要等到调用的时候。等到调用的时候,就会以传参的方式传入具体参数,此时就可以确定T的类型,然后运行函数内的代码。比如下面的调用:
Interval<Integer> integerInterval = new Interval<>(3, 4);
System.out.println(getReverse(integerInterval));
这里就是传入一个实例化后的泛型对象,传入后即可确定泛型方法的T是Integer类型,然后运行后续的代码。但是这里需要注意的是,在程序预先不知道T是什么数据类型的情况下,是不能够进行一些某些数据类型独有的运算的,比如四则运算。所以这种设计还是比较受限制的~
另外,泛型方法既可以存在于泛型类中,也可以存在于普通类中!
1.3.2、泛型方法在普通类中
还是得从一个例子看起:
/**
* 泛型方法与泛型类
* @author jiangzl
*/
public class MyGeneric2 {
public static <T> T getMiddle(T... e){
return e[e.length / 2];
}
public static void main(String[] args) {
System.out.println(getMiddle("a", "b", "c", "d", "e"));
System.out.println(getMiddle(1, 2, 3, 4, 5));
System.out.println(getMiddle(1.1, 1.2, 1.3, 1.4, 1.5));
}
}
看看输出的结果哈:
这个例子很简单,也不必多说,只是牢记一点:
① 泛型方法可以存在于普通类中
② 泛型方法的条件是方法的参数有泛型对象
③ 泛型方法需要在《返回值类型》的前面、《修饰符》的后面加上《类型参数< T > 》!
1.3.3、泛型方法在泛型类中
/**
* 泛型方法与泛型类
* @author jiangzl
*/
public class MyGeneric2 {
public static void main(String[] args) {
Sample<Integer> sample = new Sample<>();
System.out.println(sample.getEleWithT(123));
System.out.println(sample.getEleWithE("hello"));
}
}
class Sample<T> {
public T getEleWithT(T e){
return e;
}
public <E> E getEleWithE(E e){
return e;
}
}
老规矩,看看输出的结果哈:
这里其实就是 要说明一个点,很重要 :
在泛型类中写泛型方法,要注意这个类型,最好是不要和泛型类的写成一样的数据类型,否则会出错!比如说你要调用一个get()方法,但是返回的类型不是和泛型类一样的T,那么就错误了!比如下面:
/**
* 泛型方法与泛型类
* @author jiangzl
*/
public class MyGeneric2 {
public static void main(String[] args) {
Sample<Integer> sample = new Sample<>();
System.out.println(sample.getEle("hello"));
}
}
class Sample<T> {
public T getEle(T e){
return e;
}
}
这是一个错误的代码!!!
会有如下报错:
为了避免这种错误,一定要像之前那个正确的代码一样,给泛型方法一个不同于泛型类的泛型类型!
1.4、正题:泛型接口
1.4.1、设计一个泛型接口
interface AddValue<T> {
/**
* 实现两个 T 类型的对象相加
* @param e1
* @param e2
* @return e1 + e2
*/
public T add(T e1, T e2);
}
这个设计非常简单,和设计泛型类没啥区别!值得注意的是泛型接口的实现与调用方法!
1.4.2、实现一个泛型接口
public class MyGeneric1 implements AddValue<Interval<Integer>> {
public static void main(String[] args) {
Interval<Integer> interval = new Interval<>(1, 2);
Interval<Integer> integerInterval = new Interval<>(3, 4);
System.out.println((new MyGeneric1()).add(interval, integerInterval));
}
@Override
public Interval<Integer> add(Interval<Integer> e1, Interval<Integer> e2) {
return new Interval<>(e1.getLower() + e2.getLower(), e1.getUpper() + e2.getUpper());
}
}
这里大家可以看到:
① 实现一个泛型接口,相当于对接口进行实例化
② 在接口interface的声明里,无法声明static的方法,因为interface不完全等同与类,就算声明了,也和实现的类不一样!所以接口的方法都不是静态的!
③ 正是因为有第②点的存在,所以在调用实现接口后的方法的时候,都必须先实例化实现接口的类的对象,才能去调用!!!
④ 泛型可以嵌套,比如此处泛型方法addValue< T >里面的实例类型就是一个被实例化后的泛型Interval< Integer >这里只需要把这个嵌套起来的类型,看作一个整体,即可处理!
二、泛型类型的限定
2.1、什么是泛型限定?什么情况要用泛型限定?
也许在某个泛型方法中,我需要调用实例化后的泛型对象的一个方法。但是我要确保这个方法的的确确实例化后的对象有,否则就会报错:《您的对象没有这个方法……》的错误~
那么如何确保这一点呢?我们就需要泛型限定!利用这样的语法格式:
把原本简单的类型参数< T >改写成:< T extends ClassOrInterface&…>的形式!!!
这里运用了extends关键字,声明了能够进行实例化泛型方法的类,必须要是extends后面写的类名的子类或者是接口名的实现类。这样就可以保证实例化并调用这个泛型方法的对象,一定有我们需要在泛型方法中调用的对象方法!!!!这个是十分重要滴~!
举个小例子,如果我们要写一个泛型方法,这个方法需要求出一组对象数组中的,最大的元素~,那么要求这个最大的元素,我们肯定需要用compareTo() 或者是 compare() 的方法去比较!因为在Java中没有重载运算符,所以就必须要限定实例化这个泛型方法的对象,一定要实现了Comparable或者Comparetor的接口!!!那么我们看看我写的这个示例代码哈:
2.2、实例1:
import java.util.Comparator;
import java.util.Scanner;
/**
* 泛型类型的限定
* @author jiangzl
*/
public class MyGeneric3 {
public static <T extends Comparable> T getMax(T... e){
if(null == e || e.length <= 0){
return null;
}
T ret = e[0];
for(T var : e){
if(ret.compareTo(var) < 0){
ret = var;
}
}
return ret;
}
public static void main(String[] args) {
int n = 5;
Student[] s = new Student[n];
Scanner sc = new Scanner(System.in);
for(int i = 0;i < n;++i){
String name = sc.next();
int score = sc.nextInt();
s[i] = new Student(name, score);
}
sc.close();
System.out.println(getMax(s));
String Str[] = {
"b", "a", "c", "d", "e"};
System.out.println(getMax(Str));
}
}
class Student implements Comparable<Student> {
private String name;
private int score;
public Student(String name, int score){
this.name = name;
this.score = score;
}
@Override
public String toString(){
return new String("name = " + name + ", score = " + score);
}
@Override
public int compareTo(Student s) {
if(score != s.score){
return score - s.score;
}
else{
return s.name.compareTo(name);
}
}
}
还是先看看 示例1 的运行结果吧:
一个编程技巧:
第一:
这里我建议,如果是比较大小,建议要选择继承Comparable的!因为compareTo()方法是通用的,如果你选择继承Comparetor接口,那么对于String类型就不支持了!因为String类型是实现了Comparable的,用的是compareTo()方法而不是compare()方法!!!!
第二:
如果你要求能够实例化泛型方法的类必须实现多个来自不同接口的方法,你可用 & 这个符号连接extends后面的类名(接口名~)
2.3、泛型的继承关系与通配符
泛型它实际上有两个类的关系,比如说List< S > 和 ArrayList< T > 之间。我们知道,ArrayList 是 List 的子类,它们之间可以自动转型,由 ArrayList 的对象可以赋给 List 的对象。但是,即便是 S 是 T 的子类, List< S > 和 List< T >之间却是任何关系都没有的!!!那么,如果我想让 S 和 T 有父子关系的泛型对象,能够互相转换呢?我们就需要用 《通配符 ? 来完成!~》
2.3.1、先来看一个用通配符限定继承关系的示例代码
import org.omg.PortableInterceptor.ServerRequestInfo;
import java.awt.*;
/**
* 泛型里的继承与通配符
* @author jiangzl
*/
public class MyGeneric4 {
public static void getE(Vector<? extends Fruit> vector){
System.out.println("我是:" + vector.getE().getName());
}
public static void setE(Vector<? super Fruit> vector[]){
int len = vector.length;
for(int i = 0;i < len;++i) {
if(i % 2 == 0) {
vector[i].setE(new Orange("我是橘子"));
}
else{
vector[i].setE(new Apple("我是苹果"));
}
}
}
public static void main(String[] args) {
int n = 10;
Vector<Fruit>[] vector = new Vector[n];
for(int i = 0;i < n;++i){
vector[i] = new Vector<>();
}
setE(vector);
for(int i = 0;i < n;++i){
getE(vector[i]);
}
}
}
class Fruit{
private String name;
public Fruit(String name){
this.name = name;
}
public String getName(){
return name;
}
}
class Apple extends Fruit{
String name;
public Apple(String name){
super(name);
}
}
class Orange extends Fruit{
String name;
public Orange(String name){
super(name);
}
}
class Vector<T>{
private T e;
public T getE() {
return e;
}
public void setE(T e) {
this.e = e;
}
}
看看运行的结果哈:
2.3.2、说说上下界通配符
什么是上界通配符呢?语法格式如下:<? extends Fruit>
意思就是,只有Fruit类本身、或者是它的子类,才能满足条件,完成泛型实例化
什么是下界通配符呢?语法格式如下:<? super Fruit>
意思就是,只有Fruit类本身、或者是它的父类,才能满足条件,完成泛型实例化
怎样,是不是很简单呢??
2.3.3、上下界通配符的要点
其实这个说法也很容易理解,我们来理解看看哈:
① 如果是上界通配符,也就是 < ? extends Class > 这种的, 进入泛型方法的对象一定是Class本身或者是它的子类,那么就《只可以使用getter,而不能使用setter》,因为我们不能保证放进去的类型是什么类型,只知道它是Class的子类!然鹅我们要求泛型实例化后类型是确定的,所以不能够放入一个不确定的类型进去。但是我们知道,出来的类型一定是可以转换成Class类型的,只要Class类型确定,就可以转型成Class,那么就可以getter了!因为我可以用Class的对象去getter它!
比如这个例子:
② 如果是下界通配符,也就是 < ? super Class > 这种的,进入泛型的方法就是Class本身或者是它的父类,我们并不能让父类的对象赋值给子类的对象,这种转型是不允许的,所以这里就只可以:《只可以使用setter,而不能使用getter》,因为实例化泛型的类型是Class 的父类,如果是getter,不能把get到的结果赋值给Class,你不知道如何去表示这个结果,会报错!但是可以setter,因为Class确定,可以让Class本身或者它的子类赋值给Class对象。
比如这个例子:
总而言之,我们一言以蔽之~
如果你get到的,能够用Class表示出来,那就可以get。否则只能set!
三、写在后面的话
今天暂时巩固了泛型程序设计,由于本人能力有限,如果有瑕疵或者错误,还请批评指正!!感激不尽!!
明天继续研究泛型的本质,然后复习反射等知识~