JVM & JDK & JRE
JVM
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM可以理解的代码就叫做
字节码
(即扩展名为.class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。
Java 程序从源代码到运行一般有下面3步:
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
**/
- 加载Bsk02类,执行静态代码块,执行main方法里的
new Son()
- 初始化子类之前先初始化父类Father,执行静态代码块
- 初始化父类的静态变量/属性/代码块之后再执行子类Son的静态代码块
- 再构建父类对象
- 再初始化子类成员变量
- 再构建子类对象
重载和重写
重写
- 参数列表必须完全与被重写方法的相同(方法签名一致)
- 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类
- 访问权限不能比父类中被重写的方法的访问权限更低
- 父类的成员方法只能被它的子类重写
- 声明为final的方法不能被重写
- 声明为static的方法不能被重写,但是能够被再次声明
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
- 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
- 重写的方法(子类)不能抛出新的受查异常,或者比被重写方法(父类)声明的更广泛的受查异常。非受查异常没有约束。
- 构造方法不能被重写。
- 如果不能继承一个方法,则不能重写这个方法。
重载
- 被重载的方法必须改变参数列表(参数个数或类型或顺序不一样);
- 被重载的方法可以改变返回类型;
- 被重载的方法可以改变访问修饰符;
- 被重载的方法可以声明新的或更广的受查异常;
- 方法能够在同一个类中或者在一个子类中被重载。
- 无法以返回值类型作为重载函数的区分标准。
区别
区别点 | 重载方法 | 重写方法 |
---|---|---|
参数列表 | 区分标准 | 一定不能修改 |
返回类型 | 不是区分标准 | 相同或者派生类 |
受查异常 | 不是区分标准 | 可以缩小范围或删除,一定不能抛出新的或者更广的异常 |
访问 | 不是区分标准 | 一定不能做更严格的限制(可以降低限制) |
抽象类 & 接口
抽象类
- 抽象类中可以定义构造器
- 可以有抽象方法和具体方法
- 抽象类中可以定义成员变量
- 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
- 一个抽象类只能继承一个抽象类(和普通类一致)
- 抽象类可以包含静态方法
抽象方法
- 抽象方法不能同时是静态的:抽象方法需要子类重写,而静态的方法是无法被重写的(只能被重新定义),因此二者是矛盾的。
- 抽象方法不能同时是本地的:本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
- 抽象方法不能同时被synchronized修饰:synchronized加载普通方法上,拿的是对象的锁。但是抽象方法必须属于抽象类,抽象类本身不能存在实例。
接口
- 接口中不能定义构造器
- 方法全部都是抽象方法
- 抽象类中的成员可以是private、默认、protected、public
- 接口中定义的成员变量实际上都是常量
- 接口中不能有静态方法
- 一个类可以实现多个接口
接口和抽象类的区别是什么
- 接口的方法默认是 public,接口中的方法不能有实现(Java 8开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口中除了
static final
变量,不能有其他变量,而抽象类中则不一定。 - 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。
- 接口方法默认修饰符是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());
如果把Fruit
和Apple
的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。
//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>
覆盖下图中蓝色的区域。
Plate<? super Fruit>
表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。Plate<? super Fruit>
是Plate
的基类,但不是Plate
的基类。对应刚才那个例子,Plate<? super Fruit>
覆盖下图中红色的区域。
上界<? extends T>
不能往里存,只能往外取
<? extends Fruit>
会使往盘子里放东西的set(T t)
方法失效。但取东西T get()
方法还有效。比如下面例子里两个set()
方法,插入Apple
和Fruit
都报错。
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];
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成立。