Java基础总结三(泛型、异常)

Java基础总结三(泛型、异常)

泛型

泛型的创建

泛型类

我们最常用泛型的地方就是集合,因此,我们编写自己的List来体会泛型:

public class TestList<T> {

    private Object[] instances = new Object[0];

    public T get(int index) {
        return (T) instances[index];
    }

    public void set(int index,T newInstance) {
        instances[index] = newInstance;
    }

    public void add(T newInstance) {
        instances = Arrays.copyOf(instances,instances.length + 1);
        instances[instances.length - 1] = newInstance;
    }
}

看一下使用方法:

TestList<String> testList = new TestList<>();
testList.add("wlk");
String str = testList.get(0);
System.out.println(str); // wlk
泛型接口

我们除了创建泛型类,泛型接口与其很类似:

public interface Shop <T>{
    T buy();
    float refund(T item);
}

// 实现
public class RealShop<E> implements Shop<E>{
    @Override
    public E buy() {
        return null;
    }

    @Override
    public float refund(E item) {
        return 0;
    }
}

再比如,我们想要实现一个水果商店:

public class Fruit {
}

public class Apple extends Fruit{
}

public class FruitShop<E> implements Shop<E>{
    @Override
    public E buy() {
        return null;
    }

    @Override
    public float refund(E item) {
        return 0;
    }
}

FruitShop<Apple> fruitShop = new FruitShop<>(); // 这个商店卖苹果
FruitShop<Phone> stringShop = new FruitShop<>(); // 这个水果商店卖手机??? 当然不和逻辑

由于我们的商店泛型 E 没有加以限制,会导致错误,我们来给他加上限制:

public class FruitShop<E extends Fruit> implements Shop<E>{
    @Override
    public E buy() {
        return null;
    }

    @Override
    public float refund(E item) {
        return 0;
    }
}

现在这个水果商店只可以卖 Fruit 或者 Fruit 的子类,如Apple,而不能卖手机。

泛型方法

自己声明了泛型的方法

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

我们给我们的苹果商店创建一个方法,它支持用其他水果换取苹果:

public class FruitShop<E extends Fruit> implements Shop<E>{
    @Override
    public E buy() {
        return null;
    }

    @Override
    public float refund(E item) {
        return 0;
    }

    public <T extends Fruit> E exchange(T item){
        return null;
    }
}

// 使用
FruitShop<Apple> appleShop = new FruitShop<>();
Apple apple = appleShop.<Banana>exchange(new Banana());
//类型推导,<Banana>可省略
Apple apple = appleShop.exchange(new Banana());

类型擦除

泛型的好处是在编译期进行类型检查和类型转换,且泛型只在编译期有效。泛型是在 JDK 1.5 里引入的,如果不做泛型擦除,那么 JVM 需要对应使得能正确的的读取和校验泛型信息;另外为了兼容老程序,需为原本不支持泛型的 API 平行添加一套泛型 API。

在运行期,声明时的泛型参数会被擦除,在使用处的泛型会被泛型的父类替换,如<E extends Fruit>,此时使用E的地方会被Fruit替换。

泛型的协变与逆变

定义:如果A、B表示类型,f()表示一个类型的构造函数,Type1≤Type2表示Type1是Type2的子类型,Type1≥Type2表示Type1是Type2的超类型;

  • f()是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
  • f()是逆变(contravariant)的:当A≤B时有f(B)≤f(A)成立;
  • f()是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)f(B)相互之间没有继承关系。

对于任意两个不同的类型 Type1 和 Type2,不论它们之间具有什么关系,给定泛型类 G<T>G<Type1>G<Type2> 都是没有关系的,即 Java 中泛型是不变的,而数组是协变的,可以验证以下:

ArrayList<Number> list = new ArrayList<Integer>(); // 编译器报错

// 可以顺利通过编译,正常使用。
Number[] arr = new Integer[5];
arr[0] = 1;
// 但也有一定的问题,这个数组接受Number其他子类也不会在编译期报错,而在运行期报错。泛型从根源杜绝了这种错误

在Java的泛型中,是可以支持协变与逆变的,但会有很大的限制:

  • ? extends T(上边界通配符)实现协变关系,表示?是继承自T的任意子类型。也表示一种约束关系,只能提供数据,不能接收数据。
  • ? super T(下边界通配符)实现逆变关系,表示?T的任意父类型。也表示一种约束关系,只能接收数据,不能提供数据。

协变所谓只能提供数据,不能接收数据,是指在实例化出泛型类后,仅可以调用该类中返回值为泛型的方法,而不可以调用参数为该泛型的方法。

逆变则恰好相反。

那么如此限制,?在我们实际开发中应该如何应用呢?

List<? extends Number> list = new ArrayList<Integer>();

这样的用法在我们平时是用不到的,只有一些场景化的需要,才会使用到泛型通配符。就拿我们的水果商店举例,我们现在有这样一个需求,需要计算集合中水果的总重量,所以编写了如下方法:

public int totalWeight(List<? extends Fruit> list) {
    int total = 0;
    for(Fruit fruit : list) {
        total += fruit.weight;
    }
    return total;
}

// 使用起来,就可以向这个方法中传入任意Fruit子类的集合了
List<Apple> appleList = new ArrayList<>();
totalWeight(appleList);
List<Banana> bananaList = new ArrayList<>();
totalWeight(bananaList);

再举一个逆变的用法:

List<Apple> appleList = new ArrayList<Fruit>();

当然这种用法是极其少有的,不做讨论。我们给苹果类添加一个方法,这个方法的作用就是给苹果装入集合:

public class Apple extends Fruit{

    public void addToList(List<? super Apple> list) {
        list.add(this);
    }

}

这样,当我们传来Apple集合的时候,就会把Apple装入这个集合,同时,我们也可以传入Fruit的集合:

List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = new ArrayList<>();
List<Banana> bananaList = new ArrayList<>();

Apple apple = new Apple();
apple.addToList(appleList); // 编译通过
apple.addToList(fruitList); // 编译通过
apple.addToList(bananaList); // 报错

异常

异常体系

Java中异常的体系是树形结构,所有异常的超类是Throwale,它有俩个子类:Error和Exception,分别表示错误和异常,其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

  1. Error与Exception
    • Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
    • Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常,程序中应当尽可能去处理这些异常。
  2. 运行时异常和非运行时异常
    • 运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,程序中可以选择捕获处理,也可以不处理。
    • 非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过,如IOException、SQLException等以及用户自定义的Exception异常。

异常处理

  • 在try中return后,依旧会执行finally。
  • finally语句在return语句执行之后return返回之前执行的。
  • finally块中的return语句会覆盖try块中的return返回。
  • 如果finally语句中没有return语句覆盖返回值,那么原来的返回值可能因为finally里的修改而改变也可能不变(具体区分值和引用)。
  • 当发生异常后,catch中的return执行情况与未发生异常时try中return的执行情况完全一样。
  • 在Java中如果不发生异常的话,try/catch不会造成任何性能损失。在 Java 类编译后,正常流程与异常处理部分是分开的,类会跟随一张异常表,每一个try-catch都会在这个表里添加行记录。当执行抛出了异常时,首先去异常表中查找是否可以被catch,如果可以则跳到异常处理的起始位置开始处理,如果没有则原地return,并且copy异常的引用给父调用方,接着看父调用的异常表,以此类推。

我们来看一个例子:

public int test(){
    int i = 1;
    try {
        i++;
        return i;
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        System.out.println("finally");
        i++;
    }
    return 0;
}

System.out.println(test()); 

// out ->
finally
2

我们看到,finally执行了,那么i++应该也是执行了,为什么返回2,我们通过查看字节码可知

Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: istore_2
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #12                 // String finally
        12: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: iinc          1, 1
        18: iload_2
        19: ireturn
        20: astore_2
        21: aload_2
        22: invokevirtual #15                 // Method java/lang/Exception.printStackTrace:()V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: ldc           #12                 // String finally
        30: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        33: iinc          1, 1
        36: goto          53
        39: astore_3
        40: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        43: ldc           #12                 // String finally
        45: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        48: iinc          1, 1
        51: aload_3
        52: athrow
        53: iconst_0
        54: ireturn

总结一下,总体的意思就是,当执行完try块的时候,会计算return表达式的值,然后把这个返回值存在另一个临时变量里面,最后返回的时候会重新读取存放的变量,因此在finally后面无论如何修改,都不会影响返回值。那么,如果finally中加了return后,结果就会覆盖原有的返回值了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值