Java编程思想笔记0x07

泛型(二)

边界

  • 泛型边界不但可以强制规定泛型可以应用的类型,还允许泛型按照其边界类型调用方法。

通配符

逆变与协变用来描述类型转换后的继承关系,其定义为:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

  • 在Java中,数组是协变的
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class Test {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Jonathan();
        try {
            fruits[0] = new Fruit(); // Runtime Error
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            fruits[1] = new Orange(); // Runtime Error
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

上述代码可以通过编译,但是在运行时会报错。

由于早期Java不支持泛型,对数组的通用操作都是使用Object[]实现的,因此上述代码可以通过编译。但是为了避免因为声明类型和实际类型不一致而引发的问题,Java把类型检查放在了运行时

然而使用泛型时,由于擦除的存在,所有检查都必须在编译期完成,因此诸如List<Fruit> flist = new ArrayList<Apple>;的代码是无法通过编译的。与数组不同,泛型没有内建的协变类型。

但是这一限制可以使用通配符解除,例如List<? extends Fruit> flist = new ArrayList<Apple>;,这不意味着flist可以持有任意Fruit及其子类的对象,仍然需要指明其持有类型,并向上转型。

此时问题又出现了,向上转型后将无法传入任何类型对象给flist,因为编译器不知道<? extends Fruit>指向哪个类型(此时编译器会将其标记为某种未知类型),例如它可以指向Orange,那么向其中放入AppleFruitObject对象都是非法的。不过此时从flist中取出Apple对象是允许的(前提是容器中有对象)。

class Gen<T> {
    T t;

    public Test(T t) {
        // this.t = t;
        setT(t);
    }
    T getT() {
        return t;
    }
    void setT(T t) {
        this.t = t;
    }
}
class Fruit {}
class Apple extends Fruit {}

public class Test {
    public static void main(String[] args) throws Exception {
        Gen<? extends Number> g = new Gen<>(new Apple());
        g.setT(new Apple()); // error
    }
}
// Cannot infer type arguments for Test1<>
// The method setT(capture#2-of ? extends Number) in the type Test1<capture#2-of ? extends Number> is not applicable for the arguments (Apple)

在上面代码中,在构造方法中却是正确的。个人推测,new Gen<>(new Apple())这部分实际上泛型是Apple,并且也不能是包含通配符的泛型,因此构造方法顺利通过编译,但是变量g的泛型是包含通配符的,即其泛型是不确定的类型,因此后续调用setT()方法出现问题。

逆变

  • 使用超类型通配符可以允许向持有某种类型的容器中写入其子类,即指定泛型<? super ClassName>,甚至可以使用类型参数<? super T>(但不能针对类型参数给出一个超类型边界<T super ClassName>)。
public class SuperType {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
    }
}

下面代码中writeExact()无法将Apple对象放入List<Fruit>中,即使是允许的。而使用了超类型通配符的writeWithWildcard()则以把Apple对象放入List<Fruit>

class GenericWriting {
    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }
    static List<Apple> apples = new ArrayList<>();
    static List<Fruit> fruits = new ArrayList<>();
    static void f1() {
        writeExact(apples, new Apple());
        //writeExact(fruits, new Apple()); // Error
    }
    static <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item);
    }
    static void f2() {
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruits, new Apple());
    }
}

相对应的,读取的代码可以使用子类型通配符,使读取对象时实现向上转型。

class GenericReading {
    static <T> T readExact(List<T> list) {
        return list.get(0);
    }
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruits = Arrays.asList(new Fruit());
    static void f1() {
        Apple a = readExact(apples);
        Fruit f = readExact(fruits);
        f = readExact(apples);
    }

    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }
    static void f2() {
        Reader<Fruit> fruitReader = new Reader<>();
        //Fruit a = fruitReader.readExact(apples); // Error
    }
    static class CovariantReader<T> {
        T readCovariant(List<? extends T> list) {
            return list.get(0);
        }
    }
    static void f3() {
        CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
        Fruit f = fruitCovariantReader.readCovariant(fruits);
        Fruit a = fruitCovariantReader.readCovariant(apples);
    }
}

在上面的代码中,静态方法readExact()由于类型参数由list决定,所以正确返回了Apple对象并向上转型。如果只是读取,可以不使用泛型。而在f2()中,由于创建泛型类时先指定了类型参数为Fruit,因此Reader#readExact()不能接受List<Apple>参数。此时使用子类通配符即可解决问题。

无界通配符

  • 使用无界通配符表示,当前不知道(或者不需要知道)具体类型,但是不影响对其进行操作。例如可以从List<?>中取值出来(但是会丢失类型信息),不可以向其中写入。

捕获转换

  • 如果向一个使用<?>的方法传递原生类型,对于编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法,即捕获转换。
public class Main {
    static <T> T f1(List<T> list) {
        T t = list.get(0);
        return t;
    }

    static void f2(List<?> list) {
        f1(list);
    }

    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add(0);
        f1(list);
        f2(list);
    }
}

上面代码中,main()方法中调用f1(list)是有警告的,而经过f2()的捕获转换后警告消失了。


Java编程思想笔记0x06

泛型(一)

简单泛型

  • 让一个类能够持有多种类型的对象,可以使用泛型实现。

    class A<T> {
        T i;
        A(T ii) {
            i = ii;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            A<Integer> a = new A<>(1);
            int i = a.i;
            // a.i = "str test"; // 错误
            A<String> b = new A<>("test");
            String s = b.i;
        }
    }
    

示例:元组

  • 元组是指将一组对象直接打包存储于其中的一个单一对象,这个容器对象允许读取其中的元素,但是不允许向其中存放新的对象。

    public class TwoTuple<A,B> {
        public final A first;
        public final B second;
        public TwoTuple(A a, B b) {
            first = a;
            second = b;
        }
        public String toString() {
            return "(" + first + ", " + second + ")";
        }
    }
    

    当希望某个方法返回两个及以上的参数是可以使用元组来实现,创建时填入类型即可。

泛型接口

  • 泛型可以应用于接口,例如生成器:

    public interface Generator<T> {
        T next();
    }
    

泛型方法

  • 泛型同样可以在类中包含参数化方法,并且与类是否泛型无关。

  • 如果使用泛型方法可以取代整个类泛型化,那么就应该只使用泛型方法

  • 静态方法无法访问类成员中的泛型变量。如果希望静态方法拥有泛型能力,那么就需要让静态方法称为泛型方法。

  • 定义泛型方法,只需将泛型参数列表置于返回值之前:

    public <T> void f(T x){}
    
  • 调用泛型方法时,不需要指明参数类型,编译器会自动判断类型,即类型参数推断。

在Java 8以前,泛型方法的结果传递给另外一个方法时编译器不会进行推断,而Java 8 中编译器能够根据调用的方法和相应的声明来确定需要的参数类型。

匿名内部类

  • 泛型可以用于匿名内部类,同样以生成器为例:
public interface Generator<T> {
    T next();
}

public static Generator<String> generator() {
    return new Generator<String> {
        public String next() {
            return "test";
        }
    }
}

擦除

  • Java的泛型是使用擦除来实现的,这意味着在泛型代码内部,无法获得任何有关泛型参数类型的信息,即实际上,List<Integer>List<String>在运行时是相同的类型List
  • 泛型类型参数将擦除到它的第一个边界,实际上编译器会把类型参数替换为它的擦除。
  • 当希望代码能够跨多个类工作时,使用泛型才有所帮助。例如某个类有一个返回T的方法,那么泛型可以帮助该方法返回正确的类型。

补救擦除的缺陷

  • 在需要类型信息而其已经被擦除时,必须显式地传递类型的Class对象。例如,x instanceof T是错误的表达,需要x先用Class<T> ct对象保存类型信息,再比较类型信息ct.isInstnce(x)

创建泛型类型对象

  • 传递一个工厂对象到构造器中,并使用该工厂创建新对象。
class ClassAsFactory<T> {
    T x;
    public ClassAsFactory(Class<T> kind) {
        try {
            x = kind.getDeclaredConstructor().newInstance();
        } catch(Exception e) {
            System.out.println(e);
        }
    }
}
class Employee {}


public class Test {
    public static void main(String[] args) {
        ClassAsFactory<Employee> fe = new ClassAsFactory(Employee.class);
    }
}

上面代码中使用了无参构造器,如果传入的类型没有无参构造器则无法工作。

interface FactoryI<T> {
    T create();
}

class Foo<T> {
    private T x;
    public <F extends FactoryI<T>> Foo(F factory) {
    //public Foo(FactoryI factory) { // Error
        x = factory.create();
    }
}

class IntegerFactory implements FactoryI<Integer> {
    public Integer create() {
        return 0;
    }
}

class Widget {
    public static class Factory implements FactoryI<Widget> {
        public Widget create() {
            return new Widget();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        new Foo<Integer>(new IntegerFactory());
        new Foo<Widget>(new Widget.Factory());
    }
}

使用了显式的工厂对象后,可以应用于任何一种类型,并且获得了编译期检查。

abstract class GenericWithCreate<T> {
    final T element;
    GenericWithCreate() {
        element = create();
    }
    abstract T create();
}
class X {}

class Creator extends GenericWithCreate {
    X create() {
        return new X();
    }
}
public class Test {
    public static void main(String[] args) {
        Creator c = new Creator();
    }
}

上面代码使用了模板方法。

泛型数组

  • 一般情况下使用ArrayList。如果确实需要数组,则只能使用强制类型转换(T[])new Object[size]

Java编程思想笔记0x05

类型信息

Class对象

  • Java使用Class对象来执行其RTTI。
  • 类是程序的一部分,每个类都有一个Class对象
  • 类加载过程(见深入理解Java虚拟机笔记0x02
  • Class对象仅在需要的时候才被加载
  • Class.forName(String className)可以取得Class对象的引用,参数为一个包含目标类的文本名(区分大小写)。注意此方法会使类加载并初始化,作为对比,使用ClassLoader对象的loadClass()方法只会对类进行加载,而不会初始化。
  • Object对象有方法getClass()可以获取相应Class对象引用。
  • Class#getSimpleName()返回不含包名的类名,Class#getCanonicalName()返回全限定的类名。
  • Class#getSuperclass()返回其基类的Class对象

关于Class#newInstance():该方法已于Java 9声明废弃,使用Class#getDeclaredConstructor().newInstance()代替。

泛化的Class引用

  • 可以使用泛型将Class引用所指向的Class对象的类型进行限定,使其变得更为具体 。
  • Class<?>ClassClass没有表现出是否要限制Class的意思,而Class<?>则表示此处Class的限制是无限制。
  • Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查。

转型

  • Class#cast()可以将参数对象转换为Class引用的类型

类型检查

  • 关键字instanceof:判断对象是否为某种类型,用法:x instanceof ClassName。只可将其与命名类型比较,不能与Class对象比较。
  • Class#isInstance()判断参数引用是否为Class引用的实例。
  • 使用instanceof或者Class#isInstance()进行的判断是对象是否为目标类或者目标类的子类,而使用==或者equals()则表示对象是否确切的是目标类,而不是目标类的子类或者其他类。

反射

  • Java中使用Class类和java.lang.reflect共同支持反射。在类库中,Constructor用于创建新的对象,get()set()方法用于修改与Field对象关联的字段,invoke()方法用于调用与Method对象关联的方法;此外,getFields()getMethods()getConstructors()方法可以分别获得表示字段、方法以及构造器对象的数组。
  • RTTI与反射的区别:对于RTTI来说,编译器在编译时打开和检查.class文件,而对于反射来说,.class文件在编译时是不可获取的。

Java编程思想笔记0x04

字符串

不可变String

  • String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象。
  • 对于一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己。

重载“+”与StringBuilder

  • 重载:一个操作符在应用于特定的类时,赋予其特定的含义。
  • 在Java中,用于String++=是仅有的两个重载过的操作符,不允许程序员重载任何操作符。
  • Java编译器会自动优化String操作并使用StringBuilder,因为其高效,但是并不是所有的状况都会被优化。因此在需要大量操作String时使用StringBuilder是明智的选择。

无意识递归

  • 如果在toString()方法中使用this指针与其它字符串拼接,试图打印对象的地址,那么会引起无限的递归调用。[因为编译器试图把this指针转换为String类型,而转换的方法就是调用this.toString()。正确的做法是调用父类的toString(),即super.toString()

格式化输出

System.out.format()

  • 类似于C语言中的printf(),接受一个简单的格式化字符串以及一串参数。

Formatter类

  • 创建Formatter对象时需要向构造器中传入输出目标,例如System.out。使用时调用Formatter对象的format()方法。

格式化说明符

  • 抽象语法:%[argument_index$][flags][width][.precision]conversion
  • width:控制一个域的最小尺寸,在有必要时添加空格,来确保一个域至少达到某个长度。可以用于各种类型的数据转换。默认情况下数据是右对齐,可以使用-来改变对齐方向。
  • precision:指明数据的最大尺寸。不能用于所有的数据类型转换,例如整数类型,并且应用于不同的数据类型转换时意义也不同。应用于String时表示打印String时输出的字符的最大数量;应用于浮点数时,表示小数部分要显示的位数(默认是6位),多则舍入,少则补0。

Formatter转换

  • 常用的类型转换:
类型转换字符 含义
d 整数型(十进制)
c Unicode字符
b Boolean值
s String
f 浮点数(十进制)
e 浮点数(科学计数)
x 整数(十六进制)
h 散列码(十六进制)
% 字符”%”
  • 关于b转换,对于boolean基本类型和Boolean对象,其转换结果就是truefalse;对于其它类型的参数,只要其不为null,那么转换结果就是true,即使是数字0,转换结果依然是true

正则表达式

基本构造

字符 含义
B 指定字符B
\xhh 十六进制值为0xhh的字符
\uhhhh 十六进制表示为0xhhhh的Unicode字符
\t 制表符Tab
\n 换行符
\r 回车
\f 换页
\e 转义(Escape)
字符类 含义
. 任意字符
[abc] 包含a、b、c的任何字符,同 a|b|c
[^abc] 除了a、b、c的任何字符(否定)
[a-zA-Z] 从a到z或从A到Z的任何字符
\s 空白符(空格、Tab、换行、换页和回车)
\S 非空白符
\d 数字,同[0-9]
\D 非数字,[^0-9]
\w 词字符,[a-zA-Z0-9]
\W 非词字符,[^\w]
逻辑操作符 含义
XY Y跟在X后面
X|Y X或Y
(X) 捕获组,可以在表达式中使用\i来引用第i个捕获
边界匹配符 含义
^ 一行的开始
$ 一行的结束
\b 词的边界
\B 非词的边界
\G 前一个匹配的结束

量词

  • 量词描述了一个模式吸收输入文本的方式
  • 贪婪型:一次性地读入整个字符串,如果无法完成匹配就去掉最右边的一个字符再匹配,直到找到匹配的字符串或字符串的长度为0为止。它的宗旨是读尽可能多的字符,所以当读到第一个匹配时就立刻返回。
  • 厌恶型:立刻进入匹配,如果无法匹配则多读一个字符串,直到匹配成功或者字符串读完。它尽量减少了匹配到的字符串,但同样读到第一个匹配的就返回。
  • 占有型:仅匹配一次,失败不会再次尝试。

Pattern和Matcher

  • 正则表达式对象:位于java.util.regex包中,使用Pattern.compile()编译正则表达式,并返回一个Pattern对象。使用Pattern对象的matcher()方法检索一个字符串,会得到一个Matcher对象。

  • Pattern对象的split()方法可以从匹配了正则表达式的地方分割输入的字符串,返回分割后的子字符串String数组。

  • Matcher对象的find()方法可用来在CharSequence中查找多个匹配

  • 组:是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为0便是整个正则表达式,组号1表示第一对括号括起来的组。

  • Matcher对象中有许多关于组的方法:

方法名 用途
int groupCount() 返回Matcher对象中组数,不包含第0组
String group() 返回前一次匹配的第0组
String group(int i) 返回前一次匹配的指定组号,如果匹配成功但是指定的组没有匹配输入字符串的任何部分则返回null
int start(int group) 返回前一次匹配操作中寻找到的组的起始索引
int end(int group) 返回前一次匹配操作中寻找到的组的最后一个字符索引加1的值

替换

方法名 用途
String replaceFirst(String replacement) replacement替换掉第一个匹配成功的部分
String replaceAll(String replacement) replacement替换所有匹配成功的部分
Matcher appendReplacement(StringBuffer sb, String replacement) 1. 先将不匹配的地方放入sb中,即从上次读取的位置开始,到本次start() - 1;2. 将replacement放入sb中;3. 将读取位置改为end()
StringBuffer appendTail(StringBuffer sb) 在多次使用appendReplacement()方法后,使用此方法将把剩余部分的字符串直接放入sb

reset()

  • reset()可以将现有的Matcher对象应用于一个新的字符序列。

扫描输入

Scanner定界符

  • 默认情况下,Scanner对象使用空白字符对输入进行分词。使用Scanner对象的useDelimiter()并输入正则表达式作为参数可以修改成自定义的定界符。

用正则表达式扫描

  • Scanner对象的hasNext()next()方法都支持输入一个Pattern对象,找到下一个匹配该模式的部分,调用match()就能获得匹配结果。如果正则表达式中有定界符,将永远不会匹配成功。

Java编程思想笔记0x03

通过异常处理错误

基本异常

  • 异常是指组织当前方法或作用于继续执行的问题
  • 与普通问题对比:普通问题在当前环境中能得到足够的信息,总能处理错误,而对于异常则在当前环境中无法获得足够信息来解决问题。
  • 抛出异常:从当前环境跳出,并且把问题交给上一级环境
  • 异常参数:异常类有两个构造器,一个是默认构造器,另一个接受一个字符串参数,能够输入自定义的错误信息

捕获异常

try块

  • 如果不希望在方法内遇到异常就结束,可以使用try包裹可能出现异常的代码来捕获异常
  • 相较于不支持处理异常的语言,try可以不需要在每一次调用方法的前后设置错误检查的代码,从而使代码更容易编写和阅读

异常处理程序

  • 用于处理try捕获的异常,关键字为catch,参数为错误类型以及标识符。

终止与恢复

  • 终止模型:假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行
  • 恢复模型:异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并任务第二次能成功
  • 尽管恢复模型显得很吸引人,但是并不实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用代码。

自定义异常

  • 自定义异常需要从已有的异常类继承,最好是选择意思相近的异常类继承,也可以选择编程相关的所有异常的父类Exception继承。

异常说明

  • 在方法声明中,形式参数列表后使用关键字throw,后面接一个所有潜在异常类型的列表,可以表示该方法可能会抛出相应的异常。调用此方法时,如果没有处理这些异常将无法通过编译。

捕获所有异常

  • 捕获时,使用Exception类可以捕获到所有编程相关的异常。最好将其放在处理程序的末尾,以防它抢在其它异常处理程序之前把异常捕获了。
  • 栈轨迹:异常对象的getStackTrace()可以获得发生错误时调用序列,栈顶是调用序列中最后一个方法调用。
  • 重新抛出异常:在catch块中可以使用throw重新抛出异常,将异常抛给上一级环境中的异常处理程序,后续的catch子句将被忽略。

异常链

  • 异常链是指在捕获一个异常后抛出另一个异常,并且希望保存原始异常的信息。
  • Throwable的构造器可接受另一个Throwable对象作为原始异常(cause)来追踪异常最初发生的位置。
  • initCause()方法可以为一个没有设置原始异常的Throwable对象设置原始异常。注意此方法仅能调用一次,并且如果该对象已经通过构造器设置原始异常或者该方法调用超过一次,那么会抛出IllegalStateException

Java标准异常

  • 运行时异常(RuntimeException),例如空指针异常(NullPointerException)会被Java虚拟机自动抛出,不需要在异常说明中添加这类异常,如果没有被捕获,它们将直达main()方法。

使用finally清理

  • finally子句中的代码无论try块中是否抛出异常都会得到执行。
  • 当要把除内存之外的资源恢复到它们的初始状态时,可以使用finally子句。例如关闭打开的文件或者网络连接等。
  • 丢失异常:try块中的异常会被finally子句中的异常覆盖;如果在finally子句中返回,即使抛出异常也不会产生输出。

Java编程思想笔记0x02

持有对象

添加一组元素

  • Collections.addAll()方法接受一个Collection对象,以及一个数组或者一个逗号分隔符列表,将元素添加到Collection中。推荐使用此方法。
  • Arrays.asList()方法接受数组或列表,但是其返回的对象类型List并不是java.util.List,而是Arrays内的一个静态内部类,没有add()remove()方法。

List

  • ListCollection中添加了大量方法,可以在List中间插入和移除元素

类型

  • ArrayList:长于随机访问元素,但是在List中间插入和移除元素较慢
  • LinkedList:在中间插入和删除操作代价较低,提供了优化的顺序访问,但是随机访问相对较慢

部分方法

  • remove():此方法接受元素类型对象(调用其equals()方法)或者在List中的序号,来删除指定元素。注意如果List中元素为Integer类型时,删除值为x的方式为remove((Integer) x)而不是remove(x),后者会删掉序号为x的元素。
  • subList():获取子列表
  • containsAll():是否包含参数序列中所有的值。与参数顺序无关。
  • retainsALL():参数为另一个List,求两个List交集,结果保存在调用对象里。

LinkedList

  • 实现了List的基本接口,添加了可以使其用作栈、队列或双端队列的方法。

Stack

Set

类型

  • HashSet:使用散列函数实现,查找速度较快
  • TreeSet:基于红黑树实现
  • LinkedHashSet:同样使用散列函数实现,但同时用链表维护了元素插入顺序

Map

Queue

  • offer():在允许的情况下将一个元素插入队尾,或者返回false
  • PriorityQueue:使用offer()插入时会对元素进行排序。默认顺序是元素在队列中的自然顺序,可通过修改Comparator来修改顺序。

Java编程思想笔记0x01

内部类

链接到外部类

  • 内部类拥有外围对象所有成员的访问权限,以及外围类的访问权限。

获取外部类引用

  • 在内部类中,使用外部类名称以及.this可以获得外部类对象的引用。

内部类访问外部类变量

  • 如果内部类或者外围方法没有与外部类重名的变量,那么可以直接访问外部类的变量,如果有重名变量,则需要通过上述方法访问外部类变量。

创建内部类对象

  • 如果内部类不是静态的,需要通过外部类对象来创建,例如外部类Test的实例test,其包括一个内部类Inner,那么创建方式为test.new Test.Inner()

匿名内部类

  • 是指创建一个继承某个类的匿名类,通过new表达式返回的引用自动向上转型为父类的引用。
  • 可以方便快速继承类重写方法或者实现接口。

内部类的用处

  • 每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响,有效的实现了多重继承。

内部类用于回调

  • 回调函数:有些库函数需要传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数。
  • 通过内部类实现同名方法的不同实现,后通过回调可以选择向外部提供调用的方法。

内部类与控制框架

  • 应用程序框架:用于解决某类特定问题的一个类或一组类
  • 控制框架:一类特殊的应用程序框架,用来解决响应事件的请求

内部类与覆盖

  • 内部类无法被子类的同名内部类覆盖。

Java编程思想笔记0x00

对象

面向对象的程序设计方式

  1. 万物皆对象
  2. 程序是对象的集合,它们通过发送消息来告知彼此要做的。
  3. 每个对象都有自己的、由其他对象所构成的存储
  4. 每个对象都有其类型
  5. 某一特定类型的所有对象都可以接受同样的消息

封装

  • 只向客户端程序员暴露必须的部分,而隐藏其它部分
  • 原因:
    1. 让客户端程序员无法触及他们不应该触及的部分,关注对他们来说关键的东西。
    2. 允许库设计者可以改变类内部的工作方式而不必担心会影响到客户端程序员。

继承

  • 继承使用基类型和导出类型的概念表示了类型间的相似性。

多态

  • 后期绑定:当向对象发送消息时,被调用代码直到运行时才能确定。编译器确保被调用方法存在,并对调用参数和返回值执行类型检查,但并不知道被执行的确切代码。
  • 向上转型:在程序执行过程中,把导出类看做其基类的过程。

基本类型

  • 基本类型的创建:创建一个并非是引用的“自动”变量,这个变量直接存储值并置于堆栈中。
  • 基本类型占用的存储空间不随机器硬件架构变化而变化
  • 所有的数值类型都有正负号

static

  • 当声明一个事物是static时,就意味着这个域或方法不会与包含它的那个类的任何对象实例关联在一起。即使从未创建过某个类的任何对象,也可以调用其static方法或访问其staic域。
  • static内部不能调用非静态方法。

初始化与清理

构造器

  • 构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空是不同的。new表达式返回了对象的引用,但这并不是构造器的返回值。
  • 构造器中对变量的初始化会覆盖该变量在定义时的初始化
  • 如果类中没有构造器,那么编译器会自动创建一个默认的构造器,但是如果已经定义类一个构造器,无论是否有参数,编译器就不再创建默认构造器。

重载

  • 如果传入的数据类型小于方法中声明的形式参数类型,实际数据的类型就会提升。但是char型略有不同,如果无法找到恰好接受char型的方法,就会把char直接提升到int型。

初始化

  • 静态初始化只在必要的时候进行。如果不创建对象,或者不访问静态域,那么就不会进行静态初始化。
  • 初始化的顺序是先静态对象,后其它对象。

可变参数列表

  • 可以使用类似Object... args的参数形式来代替显示的声明数组。

访问权限控制

代码组织

  • 一个Java源文件称为编译单元。一个编译单元中只能有一个public类,且该类的名称需与文件名称相同(包括大小写,不包括文件名后缀)。
  • 类库中会有一组类文件,每个文件都由一个public类和任意数量的非public类组成,因此每个文件都由一个构件。如果希望这些构件从属于同一个群组,可以使用关键字package

访问权限

  • 包访问权限:是默认的访问权限,意味着当前包中的所有其它类对该成员都有访问权限。
  • 默认包:如果没有给Java文件设置任何包名称,则编译器会把处于相同目录下的Java文件看作是属于该目录下的默认包之中。
  • public:由public修饰的成员对任何类都是可用的。
  • private:由private修饰的成员,除了包含该成员的类以外,其它任何类都无法访问这个成员。
  • 如果使用privatge修饰构造器,那么将无法通过通常手段进行对象的创建,并且会阻碍对此类的继承。
  • protected:由protected修饰的成员仅支持当前类、继承类以及相同包的其它类访问(即提供包访问权限)

复用类

组合

继承

  • 继承中的构建:在继承中,构建过程是从基类流向导出类的,因此基类在导出类构造器可以访问它之前就已经完成了初始化。即使导出类没有显式创建构造器,编译器也会自动创建一个默认的构造器,该构造器将调用基类的构造器。但是如果基类设置了有参构造器,那么在导出类中如果不显示声明调用基类的某个构造器,在创建过程中将会默认调用基类的无参构造器,如果基类没有声明无参构造器则编译不会通过。

代理

  • 将一个成员对象置于要构造的类中,同时在新类中暴露该类所有的成员方法。

final

  • 使用final修饰的基本类型变量值不能改变,如果是引用类型变量则引用值不能改变,但是引用指向的对象自身是可以改变的。此外,使用final修饰的变量必须初始化(可以在声明时初始化或者在构造器中初始化)。
  • 一个既是static又是final的变量只能占据一段不能改变的存储空间,并且必须在声明时初始化。
  • 只有想要明确禁止覆盖方法时,才将方法设为final。

多态

  • 协变返回类型:在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类。

接口

抽象类

  • 抽象类和抽象方法可以使类的抽象性明确起来,并且可以明确传达类的使用方法。此外,抽象类还是有用的重构工具。

接口

  • 当组合接口时,如果存在两个方法重名、参数列表或返回值不一致,那么会产生冲突。

深入理解Java虚拟机笔记0x03

Java线程

volatile

  • 当变量声明为volatile时,变量将具备以下两个特性:
    • 保证此变量对所有线程的可见性,即一个线程修改了volatile变量后,其余线程可以立即获得修改后的值。
    • 禁止指令重排序优化,即设置内存屏障,保证volatile变量修改更新到所有CPU上

Java线程实现

  • 基于操作系统的原生线程模型。Windows和Linux下都使用一对一的线程模型。

线程状态及转换

  • 新建:创建后未启动的线程
  • 运行:正在执行或者等待CPU为其分配运行时间
  • 无限期等待:等待被其它线程显式地唤醒,不会被分配CPU时间,例如没有设置Timeout的Object.wait()或者Thread.join()
  • 限期等待:不会被分配CPU时间,但是不需要其它线程显式地唤醒,一定时间后会由系统自动唤醒,例如Thread.sleep(),设置了Timeout的Object.wait()或者Thread.join()
  • 阻塞:等待获取一个排他锁。
  • 结束:已终止的线程状态。

线程安全

线程安全程度

  1. 不可变:无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。例如final关键字修饰的变量
  2. 绝对线程安全:调用者不需要任何额外的同步措施。
  3. 相对线程安全:保证对对象的单独操作是安全的,调用者不需要做额外的保障措施。但对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性(可以保证调用对象不同的方法时,不同的方法不会交叉执行,但是无法保证一组方法的调用顺序)。例如对Vector对象同时进行remove()get()操作时可能会出现删除第i个元素后面是访问第i个元素的情况。在Java语言中,大部分的线程安全类都属于这种类型。
  4. 线程兼容:对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用。
  5. 线程对立:无论调用端是否采取同步措施,都无法在多线程环境中并发使用的代码。

线程安全的实现方法

互斥同步

  • 保证共享数据在同一时刻只被一个(使用信号量的条件下是一些)线程使用
  • 是一种悲观的同步策略
  • 在Java中使用synchronized关键字进行同步。

非阻塞同步

  • 基于冲突检测的乐观并发策略,即先进行操作,如果没有其它线程竞争,那么操作成功;反之,再采取其它补偿措施(最常见的措施是不断重试,直到成功为止),需要硬件保证操作和冲突检测两个步骤具备原子性。
  • CAS指令:当且仅当内存值和预期值相等时,使用新值更新内存值,否则不更新。
  • ABA问题:在某线程获取变量值时是A,在检查之前被改为了B,然后又恢复了A,检查时会认为该变量没有修改过。多数情况下该问题不影响程序的并发,如需解决,改为使用互斥同步可能更有效。

无同步方案

  • 可重入代码:可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),在中断返回后,原来的程序不会出现任何错误。
  • 线程本地存储:把使用共享数据的代码放入同一线程中。

锁优化

自旋锁

  • 让请求锁的线程进行短暂的等待,看锁是否很快就被释放。
  • 避免了线程切换开销,但是等待时间过长会浪费CPU资源
  • 自适应自旋锁:是否自旋、自旋时间不再确定,由前一次在同一个对象上的自旋锁时间和拥有者的状态决定。

锁消除

  • 对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。判定依据来源于逃逸分析。
  • 一些线程同步代码是内嵌的Java内部的,可以通过锁消除进行优化。

锁粗化

  • 如果有连续的对同一个对象加锁,则虚拟机会把加锁同步的范围扩大至整个操作序列外部,以减少加锁解锁带来的性能损耗。

轻量级锁

  • 使用CAS操作和对象的Mark Word实现的锁机制。
  • 前提是绝大部分的锁在同步周期内是不存在竞争的,因此CAS操作避免了使用互斥量的开销。但是如果存在锁竞争,轻量级锁比传统的互斥同步还多出CAS操作的消耗。

偏向锁

  • 该种锁偏向于第一个获得它的线程,在接下来的执行过程中,如果该锁没有被其它线程获取,则持有偏向锁的线程将永远不会进行同步。一旦有其它线程获取该锁,偏向模式即结束,转变为传统的互斥同步。

深入理解Java虚拟机笔记0x02

虚拟机类加载机制

类加载时机

类的生命周期

  • 包括:加载、验证、准备、解析、初始化、使用、卸载。其中,验证、准备、解析统称为链接。

类初始化条件(当且仅当)

  1. 遇到new、getstatic、putstatic、invokestatic字节码指令时,如果类没有初始化则触发其初始化。使用场景:
    • 使用new实例化对象
    • 读取或设置一个类的静态字段(不含被final修饰、已在编译期把结果放入常量池的静态字段)
    • 调用一个类的静态方法
  2. 对类进行反射调用的时候,如果类没有初始化则触发其初始化。
  3. 初始化一个类的时候,如果发现其父类没有初始化,则触发其父类的初始化。
  4. 虚拟机启动时会先初始化用户指定的主类(包含main()方法)。
  5. 使用动态语言支持时*。

类加载过程

加载

主要步骤

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。

加载方式

  • 对于非数组类的加载,可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器完成(重写loadClass())
  • 对于数组类的加载,主要遵循以下规则:
    • 如果数组的组件类型(去掉一个维度)是引用类型,那就递归进行组件类型的加载,数组类将在加载该组件类型的类加载器的类名称空间上被标识。
    • 如果数组的组件类型不是引用类型,虚拟机将会把数组类标记为与引导类加载器关联。
    • 数组类的可见性与它的组件类型一致,如果组件类型不是引用类型,那数组的可见性将默认为public。

验证

  • 主要为了确保Class文件的字节流中包含的信息符合当前虚拟机要求,并且不会危害虚拟机自身的安全。大致包括4个方面:文件格式验证、元数据验证、字节码验证、符号引用验证。

文件格式验证

  • 文件是否以魔数开头
  • 主、次版本号是否在当前虚拟机的处理范围内
  • 常量池中的常量是否有不支持的常量类型

元数据验证

  • 是否有父类(除java.lang.Object以外,所有类都应当有父类)
  • 父类是否继承类不允许被继承的类(被final修饰的类)
  • 非抽象类是否实现了其父类以及接口中所有要求实现的方法
  • 类中的字段、方法是否与父类产生矛盾(覆盖了父类中的final字段,重载方法参数一致但返回类型不同等不合规则的重载)

字节码验证

  • 确保操作数栈的数据类型与指令代码操作匹配
  • 确保跳转指令不会跳转到方法体以外的字节码指令上
  • 确保方法体的类型转换是有效的(父类转换为子类、或者不相干的两种类型的数据互相转换是危险的)

符号引用验证

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、字段、方法的访问性(private、protected、public,default)是否可被当前类访问

准备

  • 为类变量分配内存并设置初始值,这些变量所使用的内存将在方法区中分配。
    • 分配内存的变量仅包括被static修饰的变量,不包括实例变量。
    • 初始值为数据类型的零值,而如果变量的字段属表中存在ConstantValue属性(被final修饰)则初始值为定义的值。

解析

  • 将常量池内的符号引用替换为直接引用的过程
  • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位带目标即可。
  • 直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

  • 根据用户的程序制定的计划来初始化类变量和其他资源

类加载器

  • 类加载器是在虚拟机外实现的、通过类的全限定名来获取描述此类的二进制字节流的模块,可以让应用程序自行决定如何去获取所需要的类。
  • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即使两个类来源于同一个Class文件、同时处于同一个虚拟机中,它们的类加载器不同,那么它们就不属于同一个类。

类加载器分类

对虚拟机而言

  • 启动类加载器,是虚拟机的一部分
  • 其他类加载器,独立于虚拟机外部

对开发人员而言

  • 启动类加载器,加载<JAVA_HOME>/lib目录下,或者是虚拟机配置中指定路径的,并且被虚拟机识别的(按文件名识别)的类库。
  • 扩展类加载器,加载<JAVA_HOME>/lib/ext目录下,或被java.ext.dirs系统变量指定路径下的所有类库。
  • 应用程序类加载器,加载用户类路径下所指定的类库,如果应用程序没有指定自己的类加载器,将默认使用该类加载器。
  • 自定义类加载器。

双亲委派模型

类加载器的双亲委派

  • 如上图所示,这种类加载器的层次关系即类加载器的双亲委派模型。
  • 工作过程:如果一个类加载器收到了类加载请求,它不会先去尝试加载该类,而是把该请求委派给父类加载器去加载,每一层都是如此,即所有类加载请求都会到顶层的启动类加载器中。只有当父加载器反馈在其搜索范围内没有搜索到所需的类,无法完成加载请求时,子加载器才会尝试自己去加载。
  • 优点:使用双亲委派模型后,Java类随着它的类加载器一起具备了一种带有优先级的层级关系。例如类java.lang.Object,无论是哪一个类加载器需要加载这个类,最终都是交给启动类加载器来加载,保证了Object类在不同的类加载器环境中都是同一个类。

程序编译与代码优化

泛型

  • 真实泛型:存在于任意阶段的代码,在系统运行期间生成,有自己的虚方法和类型数据。这种现象称为类型膨胀,基于这种方法实现的泛型称为真实泛型,例如C#。
  • 伪泛型:泛型仅存在于源码,编译后仅剩原生类型(又称裸类型),并加入强制类型转换。这种方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。Java泛型是伪泛型。

即时编译

  • 在程序运行过程中,“热点代码”会被即时编译为机器码以提高运行速度,包括被多次调用的方法和被多次调用的循环体。
  • 热点探测:用于判断一段代码是否为热点代码,是否需要触发即时编译,主要有两种技术:
    • 基于采样的热点探测:周期性检查各个线程的栈顶,如果发现某个或者某些方法经常在栈顶出现,则认为这个或这些方法为热点方法。优点是实现简单,高效,容易获得方法调用关系(展开调用堆栈),缺点是难以精确的判定方法的热度,易受线程阻塞等因素影响。
    • 基于计数器的热点探测:设置计数器,统计方法调用次数;设置阈值,如果调用次数超过该阈值则认为其是热点方法。