一、this关键字
每个类都有一个隐式的变量this,再虚拟机实例化对象时自动赋值的,赋值为当前对象的“引用”。所以每个对象的this都是不一样的,this关键字可以调用本对象的成员属性,成员方法,构造方法。在一个类中,当局部变量覆盖成员变量时必须显示的用this调用成员变量,当用构造方法调用本类其他构造方法时必须显示的调用this()或this(参数)。
二、泛型
在java早期使用的是Object,但是Object涉及强转的问题,本身有很大的类型安全隐患,经常有ClassCastException的问题,最常用的例子是集合举例,如果是Object ,存入的时候可以是任何类型,但是取出时必须强转某一种类型才可以,不然只能用Object接收,如果强转只能用一种类型,如果存入两种类型的话,强转就会报类型错误。所以需要一种你存入什么类型就返回什么类型的机制,泛型就应运而生了。
所谓泛型就是把参数的类型当作参数一样可以传递,当创建对象或者调用方法的时候传过去的一种语法。泛型的<类型>只能是引用数据类型。
泛型有三种应用,泛型类,泛型接口,泛型方法。
泛型类:定义一个类时:用<T>放在类的后面,并且给全类使用。常见的比如List集合。
public class DemoClass<T> { private T t; public T getT() { return t; } public void setT(T t) { this.t = t; } }
使用的时候,直接传递具体的类型就可以,比如直接给String。
public static void main(String[] args) { DemoClass<String> d =new DemoClass<>(); }
泛型接口:和泛型类一样定义,但是子类再实现的时候有三种选择。比如我定义接口Demo1,用Demo2去实现Demo1。
public interface Demo1<T> { T show(); }
1.丢弃泛型;方法内的泛型部分全部变为Object类型;
public class Demo2 implements Demo1 { @Override public Object show() { return null; } }
2.指定一个具体类型
public class Demo2 implements Demo1<String> { @Override public String show() { return null; } }
3.继续支持泛型;(子类常见的方式)
public class Demo2<T> implements Demo1<T> { @Override public T show() { return null; } }
泛型方法:只在方法上使用泛型,不必兴师动众的在整个类中使用。
定义:
public class DemoClass { public <T> void show(T t){ System.out.println(t.toString()); } }
使用:
public static void main(String[] args) { DemoClass d =new DemoClass(); d.show("abc"); d.show(123); }
其中show方法的void可以换成任何返回类型,也就是说也可以使用T,就可以实现返回你传入的类型。
这是泛型的三种基本形式,也是我们平常的基本用法,当然还有一些高级一点的方式。比如同时多类型,通配符,限定泛型的上限,限定泛型的下限。下面一一介绍。
多类型:<T,S,E>,类似于这种形式,你可以定义好多字母,然后使用其中的任意几个。比如刚才的泛型方法:
public class DemoClass { public <T,S,E> String show(T t,S s){ return t.toString()+s.toString(); } }
前边我们用到的<T> 可传递任何引用类型,并没有明显的约束,所以叫做”无界类型“,其中的T叫做类型参数或类型变量,其实T和S等也有一些约定俗成的写法,比如(E:元素,K:键,N:数字,T:类型,V:值,S、U、V 等),如果你想你的代码很规范优雅,是需要这样写的,但是平时我们都习惯用T,E等。有的时候,那如果你不想让T代表任何类型,希望它受到一定的约束,那么需要将”无界类型“,变”有界类型“,通常使用以下形式
限定泛型的上限:形式类如<T extends UpperBoundType> ,UpperBoundType是上限,T是它的子类。
这里要说一个问题,<Object> 和<String> 什么关系?答案是没关系,尖括号里的Object 只是作为占位符,并不是真实的类别,像是形参,只是一种表示,和T一样,但是,如果<T extends Obejct>中的Object是真实的,它代表了上限类别,既然是上限类别就必须是真实存在的。
例子:
public class DemoClass<T extends Number> { }
public static void main(String[] args) { DemoClass<Integer> d =new DemoClass<>(); DemoClass<String> d =new DemoClass<String>();//报错 }
如果规定DemoClass的泛型上限是Number ,那么在使用时只能用Number的子类或者它自己,超过Number就不可以的,编译无法通过。
限定泛型的下限:<T super LowerBoundType>, LowerBoundType是下限,那么T就是它的父类。理论上是这样的,但是很遗憾,java不允许这样定义,具体的原因应该是和java实现泛型的原理有关,在后边的扩展我们介绍一下原理。
通配符:<?>,问号代表“未知类型”,注意是未知,而前面的T可以是“任何类型”,它们是不同的。同样,通配符也可以限定上限和下限,并且都是可以定义的。<?>:无限定通配符,<? extends UpperBoundType>:上限通配符,<? super UpperBoundType>:下限通配符。通常在集合中使用,比如list<?>。
使用通配符的原因有说法是由于泛型不能实现类型协变,为了能够达到那种效果所以使用通配符。那什么是协变?比如数组协变
Number[] nums = new Integer[10];
数组这样设计也是有缺陷的,虽然Integer是Number的子类,但是jvm认为n数组只能存储Integer类型的,如果我在里边放了一个Long类型的,n[1]=123L,那么运行时候就会报错,只是我们没有这么干的。
但泛型如果想实现类似于数组协变的功能,不能使用<Objecy> =<String>这样的形式,我们说过它俩没关系。那么怎么办?可以使用通配符。
List<? extends Number> l =new ArrayList<Integer>();
这样就ok了。为什么要这样?开头说过,泛型就是为了替代原始的Object形式,避免强转造成的类型安全问题,那么泛型的出现一定是要保证类型安全的,那么如果使用协变的方式就不行。
列子:猫狗动物
class Animal{}//动物 class Cat extends Animal{};//猫 class Dog extends Animal{};//狗 class JinMaoDog extends Dog{};//金毛 public class Test { public static void main(String[] args) { List<?> list =new ArrayList<>(); List<?> list2 = new ArrayList<String>(); List<?> list3 = new ArrayList<Animal>(); list.add(new Dog());//报错 list.add("123");//报错 Object o = list.get(0); } }
我们发现,使用<?>后,add方法报错了,这就是“未知”的问题,它不知道你是什么类型,也就不能添加任何类型的元素,但是可以实现类似协变的功能,可以定义String也可以定义Animal,也可以取出,因为是任何类型都有可能,所以只能用Object接收。所以List<?>这样的写法意义是什么呢?其实是把?当Object使用的,因为<Object>在泛型已经和真正的Object不一样了,那么通配符<?>的用处就显现了,所以一般用作方法的返回值声明,比如:
public <T,S> List<?> show(T t,S s){ List list =new ArrayList(); if("123".equals(t.toString())){ list.add(t); }else { list.add(s); } return list;
如上方法,如果我有多个类型传入,之后返回一个list,里边的元素根据条件存储不同的类型,那么我既不能定义LIst<T> 也不能定义List<S>,因为是“未知”的,使用List<?>,就可以解决这个问题。
使用的时候:用Object接收
public static void main(String[] args) { DemoClass d =new DemoClass(); List<?> l = d.show("123", 456); Object o = l.get(0); }
但看起来好像有回到了最原始的 的Object时代。所以我个人感觉更多的是提示作用,用以告诉使用者这里是未知的
上限通配符:
public static void main(String[] args) { List<? extends Dog> list1 = new ArrayList<Dog>(); List<? extends Dog> list2 = new ArrayList<JinMaoDog>(); List<? extends Dog> l =new ArrayList<>(); l.add(new Cat());//报错 Animal animal1 = l.get(0); Dog dog = l.get(0); }
定义的时候,我可以实现类似协变的效果,直接指定ArrayList泛型为Dog或者jinmaodog,只要不超过Dog上限就可以,但是在编译期间,jvm会进行“类型擦除”,就会变成第三个l 的形式,如此一来,?又代表了未知,你不知道是BianMuDog 还是JinMaoDog,JinMao和BianMu是同级关系,add方法仍然不可用。所以取的时候,你不能使用JinMaoDog来接受,因为如果是BianMuDog就会有类型安全问题,只能使用Dog或者Dog的父类接收才是安全的。
下限通配符:
public static void main(String[] args) { List<? super Dog> list3 = new ArrayList<Animal>(); List<? super Dog> list4 = new ArrayList<Dog>(); List<? super Dog> l =new ArrayList<>(); l.add(new Dog()); l.add(new Animal());//报错 l.add(new JinMaoDog()); Object object = l.get(0); }
和上限通配符同理,定义的时候可以指定Animal或者Dog,只要不低于Dog这个下限。区别是add方法可以用,因为无论你存入的是JinMaoDog还是BianMuDog都是低于Dog的,而指定的时候都是高于Dog的,所以你存的一定比指定的低。不会发生类型安全问题。而取的时候用Object接收,因为可以是任何类型,高于Dog也可以,低于Dog也可以。
泛型的原理
泛型的本质上是一种语法糖,何为语法糖,这个是计算机术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。比如泛型,switch语句,自动拆箱装箱,可变参数,这些本质上都是语法糖。
语法糖对应的有一个“解语法糖”,对于java泛型来说,解语法糖是发生在编译期间,就是java文件变成class文件的过程中,也就是说对于程序员来说,java文件在编辑的时候,可以直接写成<T>的形式,但它是语法糖,变成class文件后会被解开,你就看不到<T>这东西了,这个解法在Java中叫做“类型擦除”。首先将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。然后移除所有的类型参数。
比如:
public static void main(String[] args) { List<String> list =new ArrayList<>(); }
查看class文件内容变成了
public static void main(String args[])
{
java.util.List list = new ArrayList();
}
<String>被擦除了。同样在你自定义的类中<T>也会被擦除,变成Object,因为T就是任何类型,所以说jvm运行时不认识泛型的,在编译的时候会被擦除,变成最原始的类型,然后自动加强转,比如上面的代码如果在遍历集合list的话,class文件中会有强转成(String)的代码。
这时再来看<T super LowerBoundType>为什么不能定义,可能就是因为如果要擦除就必须擦掉LowerBoundType,使用它的父类型替换,可是T是不确定的,没法替换,这样就没有意义了,所以这样写不行。
这里简单介绍类型擦除,有兴趣可以去了解学习一下。