Java教程

Java基础加强三

本文主要是介绍Java基础加强三,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、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是不确定的,没法替换,这样就没有意义了,所以这样写不行。

这里简单介绍类型擦除,有兴趣可以去了解学习一下。

 

这篇关于Java基础加强三的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!