01.01.1-Java基础-基础

JVM & JDK & JRE

JVM

Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。

Java 程序从源代码到运行一般有下面3步:

我们需要格外注意的是`.class->机器码`这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT编译器的编译质量是肯定比不上JIT编译器的。

总结:

Java虚拟机(JVM)是运行Java字节码的虚拟机。JVM有针对不同系统的特定实现(Windows、Linux、macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是Java语言”一次编译,随处可以运行“的关键所在。

JDK 和 JRE

JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。

JRE是Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括Java虚拟机(JVM),Java类库,Java命令和其他的一些基础构件。但是,它不能用于创建新程序。

如果只是为了运行一下Java程序的话,那么只需要安装JRE。但如果需要进行一些Java编程方面的工作,那么就需要安装JDK了。但是,这不是绝对的。有时,即使不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要部署JSP-Web应用程序,需要JDK来将JSP编译成为Servlet。

异常

异常是程序在运行时出现的会导致程序运行终止的错误。这种错误是不能通过编译系统检查出来的。常见的异常的发生原因为系统资源错误和用户操所错误两种。

Java把异常信息封装成一个类。当发生某种异常时将某种对应的类作为异常信息抛出。异常的根类时Throwable,其有两个直接子类 Error和Exception。Error是描述系统资源错误的类。Exception是描述用户操作错误的类。

发生了Error就是必须修改代码或调整外部环境问题,程序肯定会终止。比如需要开辟一个内存大小为99999999个int的数组。所以一般来说Error很少发生。发生了Exception就要进行处理,使程序运行下去,如数组越界异常,文件不存在异常等。而Exception又可以分为两种,一种是程序本身存在的问题引发的异常(健壮性不够),即:RuntimeException;一种是程序本身可能没有问题,但遇到诸如文件不存在所导致的错误,Excption中除了RuntimeException外都是此种异常,此种异常被称为受查异常(受查异常在写代码的时候会提示抛出还是处理)。Error+RuntimeException构成了非受查异常。

**受查异常、非受查异常**

Java语言规范规定派生于Error类或RuntimeException类的异常都称为非受查异常,其余异常都被成为受查异常。受查异常就是必须告诉它的调用者可能会出现异常,让其调用者抛出或者捕获处理,这类异常如果没有在程序中进行异常处理,编译不通过。非受查异常则不需要。

声明受查异常(throws)

除了Error和RuntimeException的异常都是受查异常,这些异常如:IOException、SQLException等,在可能发生的方法中需要被声明。同时非受查异常最好不要被声明。需要使用throws声明异常后的情况如下:

  • 调用一个抛出受查异常的方法:public int read() throws IOException
  • 方法中使用throw语句抛出了受查异常。

抛出异常(throw)

throw可以用于抛出异常对象。格式如下例:

public static void demo() throws Exception{    
	throw new Exception("出现异常");
}

常见的运行时异常

  • java.lang.NullPointerException :空指针异常;

    • 出现原因:调用了未经初始化的对象或者是不存在的对象。
  • java.lang.ClassNotFoundException:指定的类找不到;

    • 出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
  • java.lang.NumberFormatException:字符串转换为数字异常;

    • 出现原因:字符型数据中包含非数字型字符。
  • java.lang.IndexOutOfBoundsException:数组角标越界异常,常见于操作数组对象时发生。

  • java.lang.ClassCastException:数据类型转换异常

  • SQLException:SQL 异常,常见于操作数据库时的 SQL 语句错误。

final

修饰类

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

修饰方法

只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的。

重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义方法签名相同的方法,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)

修饰变量

final成员变量表示常量,只能被赋值一次,赋值后值不再改变。

  • 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;
  • 如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。

final修饰一个成员变量,必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。

finalize

Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。

static

静态方法不可以覆盖,但是可以被重新定义(注意,静态方法可以被继承)

public class Bks01 extends Dad {
    public static void main(String[] args) {
        test();             // son
        Dad.test();         // dad
    }
    // static方法可以被重新定义
    static void test() {	System.out.println("son");	}
}

class Dad {
    static void test() {	System.out.println("dad");	}
}

static执行顺序

  • 静态先于普通
  • 父类先于子类
public class Bsk02 {
    static {	System.out.println("Bsk02 static");	}
    public Bsk02() {	System.out.println("Bsk02 constructor");	}
    public static void main(String[] args) {	
        new Son();	
    }
}

class Son extends Father {
    Bsk02 bsk02 = new Bsk02();
    static {	System.out.println("Son static");	}
    public Son() {	System.out.println("Son constructor");	}
}

class Father {
    static {	System.out.println("Father static");	}
    public Father() {	System.out.println("Father constructor");	}
}
/**
    Bsk02 static
    Father static
    Son static
    Father constructor
    Bsk02 constructor
    Son constructor
**/
  1. 加载Bsk02类,执行静态代码块,执行main方法里的new Son()
  2. 初始化子类之前先初始化父类Father,执行静态代码块
  3. 初始化父类的静态变量/属性/代码块之后再执行子类Son的静态代码块
  4. 再构建父类对象
  5. 再初始化子类成员变量
  6. 再构建子类对象

重载和重写

重写

  • 参数列表必须完全与被重写方法的相同(方法签名一致)
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类
  • 访问权限不能比父类中被重写的方法的访问权限更低
  • 父类的成员方法只能被它的子类重写
  • 声明为final的方法不能被重写
  • 声明为static的方法不能被重写,但是能够被再次声明
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
  • 重写的方法(子类)不能抛出新的受查异常,或者比被重写方法(父类)声明的更广泛的受查异常。非受查异常没有约束。
  • 构造方法不能被重写。
  • 如果不能继承一个方法,则不能重写这个方法。

重载

  • 被重载的方法必须改变参数列表(参数个数或类型或顺序不一样);
  • 被重载的方法可以改变返回类型;
  • 被重载的方法可以改变访问修饰符;
  • 被重载的方法可以声明新的或更广的受查异常;
  • 方法能够在同一个类中或者在一个子类中被重载。
  • 无法以返回值类型作为重载函数的区分标准。

区别

区别点重载方法重写方法
参数列表区分标准一定不能修改
返回类型不是区分标准相同或者派生类
受查异常不是区分标准可以缩小范围或删除,一定不能抛出新的或者更广的异常
访问不是区分标准一定不能做更严格的限制(可以降低限制)

抽象类 & 接口

抽象类

  • 抽象类中可以定义构造器
  • 可以有抽象方法和具体方法
  • 抽象类中可以定义成员变量
  • 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
  • 一个抽象类只能继承一个抽象类(和普通类一致)
  • 抽象类可以包含静态方法
抽象方法
  • 抽象方法不能同时是静态的:抽象方法需要子类重写,而静态的方法是无法被重写的(只能被重新定义),因此二者是矛盾的。
  • 抽象方法不能同时是本地的:本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
  • 抽象方法不能同时被synchronized修饰:synchronized加载普通方法上,拿的是对象的锁。但是抽象方法必须属于抽象类,抽象类本身不能存在实例。

接口

  • 接口中不能定义构造器
  • 方法全部都是抽象方法
  • 抽象类中的成员可以是private、默认、protected、public
  • 接口中定义的成员变量实际上都是常量
  • 接口中不能有静态方法
  • 一个类可以实现多个接口

接口和抽象类的区别是什么

  1. 接口的方法默认是 public,接口中的方法不能有实现(Java 8开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
  2. 接口中除了static final变量,不能有其他变量,而抽象类中则不一定。
  3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。
  4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。

备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。

对象克隆

多层浅拷贝

public class CloneTest{
	public static void main(String[] args) throws CloneNotSupportedException {
		Body body = new Body(new Head(new Face(new String("丑"))));
		Body body1 = (Body) body.clone();
		System.out.println("body == body1 : " + (body == body1));
		System.out.println("body.head == body1.head : " + (body.head == body1.head));
		System.out.println(body.head.face == body1.head.face);
		System.out.println(body.head.face.name == body1.head.face.name);
	}
}

class Body implements Cloneable {
	public Head head;
	public Body(Head head) { this.head = head; }

	@Override
	protected Object clone() throws CloneNotSupportedException {
		Body newBody = (Body) super.clone();
		newBody.head = (Head) head.clone();
		return newBody;
	}
}

class Head implements Cloneable {
	public Face face;
	public Head(Face face) { this.face = face; }

	@Override
	protected Object clone() throws CloneNotSupportedException {
		Head newHead = (Head)super.clone();
		newHead.face = (Face) face.clone();
		return newHead;
	}
}

class Face implements Cloneable{
	
	public Face(String name) { this.name = name; }
	public String name;

	@Override
	protected Object clone() throws CloneNotSupportedException {
		Face newFace = (Face)super.clone();
		newFace.name = new String(this.name);
		return newFace;
	}
}

序列化

public class CloneTest {
	public static void main(String[] args) {
		try {
			Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));
			Person p2 = CloneUtils.clone(p1); // 深度克隆
			p2.getCar().setBrand("BYD");
			System.out.println(p1);
			System.out.println(p2);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

class CloneUtils {
	private CloneUtils() {
		throw new AssertionError();
	}

	@SuppressWarnings("unchecked")
	public static <T extends Serializable> T clone(T obj) throws Exception {
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bout);
		oos.writeObject(obj);
		ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
		ObjectInputStream ois = new ObjectInputStream(bin);
		return (T) ois.readObject();
	}
}

class Person implements Serializable {
	private static final long serialVersionUID = -9102017020286042305L;
	private String name; // 姓名
	private int age; // 年龄
	private Car car; // 座驾

	public Person(String name, int age, Car car) {
		this.name = name;
		this.age = age;
		this.car = car;
	}
    
	// ... getter方法和setter方法

	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
	}
}

class Car implements Serializable {
	private static final long serialVersionUID = -5713945027627603702L;
	private String brand; // 品牌
	private int maxSpeed; // 最高时速

	public Car(String brand, int maxSpeed) {
		this.brand = brand;
		this.maxSpeed = maxSpeed;
	}

	// ... getter和setter方法

	@Override
	public String toString() {
		return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
	}
}

泛型

泛型类

// 此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
// 在实例化泛型类时,必须指定T的具体类型
public class Generic<T> {
    // key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    // 泛型构造方法形参key的类型也为T,T的类型由外部指定
    public Generic(T key) { this.key = key; }

    // 泛型方法getKey的返回值类型为T,T的类型由外部指定
    public T getKey(){ return key; }
    
    public static void main(String[] args) {
    	// 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
    	// 传入的实参类型需与泛型的类型参数类型相同,即为Integer
    	Generic<Integer> genericInteger = new Generic<Integer>(123456);

    	// 传入的实参类型需与泛型的类型参数类型相同,即为String
    	Generic<String> genericString = new Generic<String>("key_vlaue");
    	System.out.println("key is " + genericInteger.getKey().getClass());
    	System.out.println("key is " + genericString.getKey().getClass());
	}
    
}

泛型接口的实现

// 定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() { return null; }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next(); 中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {
    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型方法

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

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 *	   5)如果泛型类也声明了一个T,泛型方法的T会覆盖泛型类的T
 */
public <T> T genericMethod(Class<T> tClass)
    	throws InstantiationException, IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
泛型"方法"的详细举例
public class GenericTest {
   // 这个类是个泛型类,在上面已经介绍过
   public class Generic<T> {
        private T key;

        public Generic(T key) { this.key = key; }

        // 我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        // 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        // 所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){ return key; }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
            public E setKey(E key){
                 this.key = keu
            }
        */
    }

    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        // 当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
        public <T> T showKeyName(Generic<E> container){
            ...
        }  
     */
}
类中的泛型方法
public class Generic {
    class Fruit {
        public String toString() { return "fruit"; }
    }

    class Apple extends Fruit {
        public String toString() { return "apple"; }
    }

    class Person {
        public String toString() { return "Person"; }
    }

    class GenerateTest<T> {
        // 方法中的T和类上声明的T一致
        public void show_1(T t){
            System.out.println(t.toString());
        }

        // 泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        // 由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛
        // 型方法中识别的泛型。
        public <E> void show_3(E t) {
            System.out.println(t.toString());
        }

        // 泛型类中声明了一个泛型方法,使用泛型T,这个T是一种全新的类型,即泛型方法的T覆盖了泛型类的T
        // 编译器会报警告:The type parameter T is hiding the type T
        public <T> void show_2(T t) {
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Generic().new Apple();
        Person person = new Generic().new Person();

        GenerateTest<Fruit> generateTest = new Generic().new GenerateTest<Fruit>();
        
        // apple是Fruit的子类,所以这里可以,此时实际上是多态的性质
        generateTest.show_1(apple);
        // 编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        // generateTest.show_1(person);

        // 使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        // 使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

静态方法与泛型

类中的静态方法使用泛型:**静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。**即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

public class StaticGenerator<T> {
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

泛型通配符

泛型类和泛型方法都是定义了一个类或者方法的模板。能构造不同类型的属性或不同参数类型的方法。我们假设:

class Fruit {}
class Apple extends Fruit {}

然后有一个最简单的容器:Plate类。盘子里可以放一个泛型的“东西”。我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。

class Plate<T> {
    private T item;
    public Plate(T t) {	item = t;	}
    public void set(T t){	item = t;	}
    public T get(){	return item;	}
}

现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。

Plate<Fruit> p = new Plate<Apple>(new Apple());  // error

但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。 因为在泛型类构造具体类的时候,等号前面构造的是private Fruit p;,后面构造的是private Apple p;,这两者肯定无法相等,所以编译器会报错。

为了让泛型用起来更舒服,Sun的大脑袋们就想出了的<? extends T><? super T>办法,来让”水果盘子“和”苹果盘子“之间发生关系。

上下界通配符

下面代码就是“上界通配符(Upper Bounds Wildcards)”:

Plate<? extends Fruit>

翻译成人话就是:啥水果都能放的盘子。Plate<? extends Fruit>,这样我们可以用“苹果盘子”给“水果盘子”赋值了。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

如果把FruitApple的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。

//Lev 1
class Food{}

//Lev 2
class Fruit extends Food{}
class Meat extends Food{}

//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}

//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}

在这个体系中,下界通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域。

相对应的,“下界通配符(Lower Bounds Wildcards)”:
Plate<? super Fruit>

表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。Plate<? super Fruit>Plate的基类,但不是Plate的基类。对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。

##### 缺陷

上界<? extends T>不能往里存,只能往外取

<? extends Fruit>会使往盘子里放东西的set(T t)方法失效。但取东西T get()方法还有效。比如下面例子里两个set()方法,插入AppleFruit都报错。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

// 不能存入任何元素
p.set(new Fruit());    // Error
p.set(new Apple());    // Error

// 读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get();    // Error

下界<? super T>不影响往里存,但往外取只能放在Object对象里

使用下界<? super Fruit>会使从盘子里取东西的get()方法部分失效,只能存放到Object对象里。set()方法正常。

Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());

//存入元素正常
p.set(new Fruit());
p.set(new Apple());

//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get();    //Error
Fruit newFruit1=p.get();    //Error
Object newFruit2=p.get();

PECS

至于为什么通配符有缺陷,笔者找了很多资料也没弄明白。不过从记忆的角度说

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

PECS:站在方法体的角度,Producer Extends Consumer Super。

无限定通配符

无限定通配符和不使用通配符的区别是,不使用通配符在set的时候可以传入任意对象,使用通配符后任何对象都无法传入(包括Object)。

泛型的缺陷

只能使用类类型

泛型擦除后会用Object替代,但是Object只能引用类类型。

运行时类型查询只适用于原始类型
if(a instanceof Pair<String>);	 	// error
Pair<String> p = (Pair<String>) a;	// error
ArrayList<String>.class == ArrayList<Integer> // true
不能创建一个确切的泛型类型的数组

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];

这样也是可以的:

List<String>[] ls = new ArrayList[10];

下面使用Sun的一篇文档的一个例子来说明这个问题:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

/**
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
*/

下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
可变参参数警告
public static <T> void addAll(Collection<T> coll, T... ts) {
	for (T : ts)
		coll. add(t);
}

应该记得,实际上参数ts是一个数组,包含提供的所有实参,现在考虑一下调用:

Collection<Pair<String>> table ...;
Pair<String> pairl = ...;
Pair<String> pair2 = ...;
addAll(table, pairl, pair2);

为了调用这个方法,Java虚拟机必须建立一个Pair <String>数组,这就违反了前面的规则。不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。可以采用两种方法来抑制这个警告。一种方法是为包含addA1调用的方法增加注解@Suppress Warnings("unchecked")或者在Java SE7中,还可以用@Safe Varargs直接标注addAll方法:

@SafeVarargs
public static <T> void addAl1 (Collection<T> coll, T... ts)

现在就可以提供泛型类型来调用这个方法了。对于只需要读取参数数组元素的所有方法,都可以使用这个注解。

不能实例化类型变量

不能使用像new T(...)new T[...]T.class这样的表达式中的类型变量。例如,下面的Pair<T>构造器就是非法的:

public Pair { 	first= new T(); second = new T();	}		// Error
Java8之后解决方案
public class Pair<T> {

    public T first;
    public T second;

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

    public static <T> Pair<T> makePair(Supplier<T> constr){
        return new Pair<>(constr.get(), constr.get());
    }

    public static void main(String[] args) {
        Pair<String> p = makePair(() -> "aaa");
        System.out.println(p.first);
        System.out.println(p.second);
    }
}
反射解决
public static <T> Pair<T> makePair(Class<T> cla){
    try {
        return new Pair<>(cla.newInstance(), cla.newInstance());
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}
不能实例化类型数组
//    T [] ts = new T[100];     error
Java8解决方案
public class Pair<T> {
    T[] ts;

    public Pair(T[] ts) {	this.ts = ts;	}

    public static <T> Pair<T> makePair (IntFunction<T[]> constr) {
        return new Pair<>(constr.apply(1));
    }

    public static void main(String[] args) {
        Pair<String> p = Pair.makePair(value -> new String[value]);
    }
}
反射解决
public static <T> Pair<T> makePair(T... a) {
    return new Pair<>((T[]) Array.newInstance(a.getClass().getComponentType(), 2));
}
数组作为类的私有实例域

如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object [],并且在取元素时进行类型转换。 例如ArrayList类这样实现:

transient Object[] elementData;

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
    return (E) elementData[index];
}

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}
泛型类的静态上下文中类型变量无效

不能在静态域或方法中引用类型变量。 例如,下列高招将无法施展:

public class Singleton <T>	{
	private static T singlelnstance ; // Error
	public static T getSinglelnstance() // Error
	{
		if (singleinstance == null) construct new instance of T
            return singlelnstance ;
	}
}
不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类对象。 实际上,甚至泛型类扩展Throwable都是不合法的。例如,以下定义就不能正常编译:

public class Problem <T> extends Exception { /*...*/ } // Error can't extend Throwable

catch 子句中不能使用类型变量。 例如,以下方法将不能编译:

public static <T extends Throwable> void doWork(Class<T> t) {
	try	{
		// do work
	} catch (T e) { // Error can't catch type variable
		// ...
	}
}

不过,在异常规范中使用类型变量是允许的。以下方法是合法的:

public static <T extends Throwable> void doWork(T t) throws T { // OK
	try { 
		// do work
	} catch (Throwable realCause) {
		throw t ;
	}
}
可以消除对受查异常的检查

Java 异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。关键在于以下方法:

@SuppressWamings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T {
	throw (T) e ;
}

假设这个方法包含在类Block中 , 如果调用Block.<RuntimeException> throwAs(t);,编译器就会认为t是一个非受查异常。以下代码会把所有异常都转换为编译器所认为的非受查异常:

try {
	// do work
} catch (Throwable t) {
	Block.<RuntimeException> throwAs(t);
}

下面把这个代码包装在一个抽象类中。用户可以覆盖body方法来提供一个具体的动作。调用toThread时,会得到Thread类的一个对象,它的run方法不会介意受查异常。

public abstract class Block {
	public abstract void body() throws Exception;
	public Thread toThread() {
        return new Thread(){
			public void run(){
                try {
                    body();
                } catch (Throwable t) {
                    Block.<RuntimeException> throwAs(t);
                }
            }
        };
    }

    @SuppressWamings("unchecked")
	public static <T extends Throwable> void throwAs(Throwable e) throws T {
		throw (T) e;
    }
}

例如,以下程序运行了一个线程,它会拋出一个受查异常。

public class Test {
	public static void main(String []args) {
		new Block() {
			public void body() throws Exception {
				Scanner in = new Scanner(new File("ququx"), "UTF-8");
				while(in.hasNext())
					System.out.println(in.next());
			}
		}.toThread().start();
	}
}

运行这个程序时,会得到一个栈轨迹,其中包含一个FileNotFoundException。这有什么意义呢?正常情况下,你必须捕获线程run方法中的所有受查异常 , 把它们“包装”到非受查异常中,因为run方法声明为不抛出任何受查异常。

不过在这里并没有做这种“包装”。我们只是抛出异常,并“哄骗”编译器,让它认为这不是一个受查异常。通过使用泛型类、 擦除和@SuppressWamings注解,就能消除 Java 类型系统的部分基本限制。

<T extends Comparable<? super T>>

 public class Test	{
     //	第一种声明:简单,灵活性低
     public static <T extends Comparable<T>> void mySort1(List<T> list)	{
         Collections.sort(list);
     }
     //	第二种声明:复杂,灵活性高
     public static <T extends Comparable<? super T>> void mySort2(List<T> list) {
         Collections.sort(list);
     }
     public static void main(String[] args) {
     	//主函数中将分别创建Animal和Dog两个序列,然后调用排序方法对其进行测试
    	//main函数中具体的两个版本代码将在下面具体展示
     }
 }

 class Animal implements Comparable<Animal> {
     protected int age;
     public Animal(int age) {
         this.age = age;
     }
     //使用年龄与另一实例比较大小
     @Override
     public int compareTo(Animal other) {
         return this.age - other.age;
     }
 }

 class Dog extends Animal {
     public Dog(int age) {
         super(age);
     }
 }

对mySort1()进行测试,main方法代码如下所示

// 创建一个 Animal List
List<Animal> animals = new ArrayList<Animal>();
animals.add(new Animal(25));
animals.add(new Dog(35));

// 创建一个 Dog List
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog(5));
dogs.add(new Dog(18));

// 测试  mySort1() 方法
mySort1(animals);
mySort1(dogs);		// error

结果编译出错,报错信息为:

The method mySort1(List<T>) in the type TypeParameterTest is not applicable for the arguments (List<Dog>)

如果传入的是List<Animal>程序将正常执行,因为Animal实现了接口Comparable<Animal>。但是,如果传入的参数是List<Dog>程序将报错,因为Dog类中没有实现接口Comparable<Dog>,它只从Animal继承了一个Comparable<Animal>接口。

对mySort12()进行测试,main方法代码如下所示

// 创建一个 Animal List
List<Animal> animals = new ArrayList<Animal>();
animals.add(new Animal(25));
animals.add(new Dog(35));

// 创建一个 Dog List
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog(5));
dogs.add(new Dog(18));

// 测试  mySort2() 方法
mySort2(animals);
mySort2(dogs);

这时候我们发现该程序可以正常运行。它不但能够接受Animal implements Comparable<Animal>这样的参数,也可以接收Dog implements Comparable<Animal>这样的参数。

  • 在添加animals时,T是Animal,Comparable的泛型是Animal,Animal super Animal成立。
  • 在添加dogs时,T是Dog,Comparable的泛型是Animal,Animal super Dog成立。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值