CH02-基本知识

数据类型

包装类型

八个基本类型:

  • boolean:1
  • byte:8
  • char:16
  • short:16
  • int:32
  • float:32
  • long:64
  • double:64

基本类型都拥有对应的包装类型,它们之间的赋值使用自动装箱与拆箱完成:

Integer x=2;	// 装箱
int y=x;			// 拆箱

缓存池

new Integer(21)Integer.valueOf(21) 的区别在于:

  • 前者每次都会创建一个新的对象
  • 后者会使用缓存池中的对象,多次调用会获得同一个对象的引用
Integer x = new Integer(21);
Integer y = new Integer(21);
System.out.println(x == y);	// false
Integer m = Integer.valueOf(21);
Integer n = Integer.valueOf(21);
System.out.println(m == n);	// true

valueOf() 方法的实现比较简单,它会先判断值是否在缓存池中,有则返回否则新建并返回。

public static Integer valueOf(int i) {
  if(i >= IngegerCache.low && i <= Integer.high){
    return IntegerCache[i + (-IngegerCache.low)];
  }
  return new Integer(i);
}

在 Java 8 中,Integer 缓存池的大小默认为 -128 至 127。

编译器会在缓冲池范围内的基本类型自动装箱过程中调用 valueOf 方法,因此多个取值相同的 Integer 实例在使用自动装箱来创建时,会引用相同的对象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

boolean: true, false

all byte values

short values between -128 to 127

int values between -128 to 127

char in range \u0000 to \u007F

String

概览

String 被声明为 final,因此不可被继承。

其内部使用 char 数组存储数据,该数据也被声明为 final,表示该 value 数组初始化之后便不能再被修改。并且没有提供修改该数组的方法,因此可以保证 String 不可变。

不可变的好处

  1. 可以缓存哈希值

因为 String 的 hash 值经常被使用,比如 String 作为 HashMap 的 key。不可变使得其 hash 值也不会变,因此仅需计算一次。

  1. String Pool 的需要

如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。

  1. 安全性

String 经常被作为参数类型,String 不可变性可以保证参数不可变。

  1. 线程安全

不可变特性天生具备线程安全性,可以在多个线程中并发使用。

Program Creek: Why String is immutable in Java?

String, StringBuffer, String Builder

  • 可变性
    • String 不可变
    • StringBuffer、StringBuilder 可变
  • 线程安全
    • String 不可变,即线程安全
    • StringBuilder 非线程安全
    • StringBuffer 保证线程安全,内部使用 synchronized 实现同步

StackOverflow : String, StringBuffer, and StringBuilder

Stirng.intern()

使用 Stirng.intern() 可以保证相同内容的字符串变量引用同一个内存对象。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
System.out.println(s1.intern() == s3);  // true

如果直接使用 "bbb" 这种形式而非 new 来创建字符串实例,会自动将其放入 String Pool 中。

String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5);  // true

在 Java 7 之前,字符串常量池被放在运行时常量池中,属于永久代。在 Java 7 中,字符串常量池被移到 Native Method 中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OOM。

运算

参数传递

Java 中的参数是以值传递的形式传入方法中,而不是引用传递。(这里的值值得是引用的地址值)

以下代码中,Dog dog 是一个指针,存储的是对象的地址值。在将一个参数传入一个方法时,本质上是将对象的地址以值的形式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针将指向不同的对象,一方改变自身指向的对象不会对另一方产生影响。

public class Dog {
    String name;

    Dog(String name) {
        this.name = name;
    }

    String getName() {
        return this.name;
    }

    void setName(String name) {
        this.name = name;
    }

    String getObjectAddress() {
        return super.toString();
    }
}

public class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        func(dog);
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        System.out.println(dog.getName());          // A
    }

    private static void func(Dog dog) {
        System.out.println(dog.getObjectAddress()); // Dog@4554617c
        dog = new Dog("B");
        System.out.println(dog.getObjectAddress()); // Dog@74a14482
        System.out.println(dog.getName());          // B
    }
}

但是如果在方法中改变对象的字段值,则会改变原对象的字段值,因为改变的是同一个地址指向的内容。

class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        func(dog);
        System.out.println(dog.getName());          // B
    }

    private static void func(Dog dog) {
        dog.setName("B");
    }
}

StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?

float 与 double

1.1 这样的字面量属于 double 类型,不能直接将其赋值给 float 变量,因为是向下转型。Java 不能隐式执行向下转型,因为会使得精度降低。

float f = 1.1;  // error
float m = 1.1f; // right

隐式类型转换

因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式的将 int 类型向下转型为 short 类型。

但是使用 += 运算可以执行隐式类型转换:

short s1 = 1;
// s1 = s1 + 1;

s1 += 1;

这其实相当于将 s1 + 1 的计算结果执行了向下转型:

s1 = (short) (s1 + 1);

StackOverflow : Why don’t Java’s +=, -=, *=, /= compound assignment operators require casting?

switch

从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。

Switch 不支持 long,是因为 switch 的设计初衷是对那些只有极少数的几个值进行等值判断,如果值的范围过大,那么还是使用 if 比较合适。

Why can’t your switch statement data type be long, Java? - Stack Overflow

继承

访问权限

Java 中有三个访问权限修饰符:private、protected、public,如果不加访问修饰符,则表示 package 范围内可见。

可以对类或类中的成员(字段/方法)添加修饰符。

  • 类可见表示其他类可以用这类来创建实例对象。(即通过类来引用成员)
  • 成员可见表示其他类可以用这个类的实例对象访问到该成员。(即通过类实例来引用成员)

protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是该修饰符对于类没有作用。

设计良好的模块会隐藏所有实现细节,将其 API 和实现细节清晰的隔离开来。模块之间仅通过他们的 API 进行通信,一个模块不需要知道其他模块内部的具体细节,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能的使每个类或成员不被外界访问。

如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以在使用父类实例的地方都能使用子类实例,即子类实例提供了父类应该具体的能力,即里氏替换原则。

字段决不能是 public,因为这么做就是去了对这个字段修改行为的控制,外部可以对其执行任意修改。

如果是 package 范围或私有的嵌套类,那么直接暴露成员不会有太大的影响。

抽象类与接口

  1. 抽象类

抽象类和抽象方法使用 abstract 关键字声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。

抽象类和普通类之间最大的区别是,抽象类不能被实例化,需要继承抽象类并实例化子类。

  1. 接口

接口是抽象类型的延伸,在 Java 8 之前,可以看做是一个完全抽象的类,即不能拥有任何实现方法。

从 Java 8 开始,接口也可以拥有默认的实现方法,这是因为不支持默认方法的接口的维护成本太高了。在此之前,如果一个接口想要添加新方法,那么要修改所有实现了该接口的类。

接口的成员都是 public 可见,并且不允许定义为 private 或 protected。

接口的字段默认都是 static final 的,即静态的。

  1. 比较
  • 从设计层面看,抽象类提供了 IS-A 关系,那么就必须满足里氏替换原则,即子类对象必须能够替换所有父类对象。
    • 接口更像是 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
  • 从使用上来看,一个类可以实现多个接口,但不能同时继承多个抽象类。
  • 接口的字段只能是 static final,抽象类没有该限制。
  • 即可的成员只能是 public 可见,而抽象类的成员可以有多种范围可见。
  1. 选择

使用接口:

  • 需要让不相关的类都都实现一个方法,比如无关的类都可以实现 Comparable 接口中的 compareTo 方法。
  • 需要使用多重继承。

使用抽象类:

  • 需要在几个相关的类中共享代码。
  • 需要能够控制继承的成员的访问权限,而非均为 public。
  • 需要集成非静态和非常量字段。

在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活的为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的实现方法,使得修改接口的成本降低。

super

  • 访问父类的构造函数:从而委托父类完成一些初始化工作。
  • 方位父类的成员:如果子类重写了父类的某个方法实现,则可以通过 super 来引用父类原有的方法实现。

Using the Keyword super (opens new window) (oracle.com)

重写与重载

  1. 重写(overwrite)

存在与继承体系中,子类实现了与父类在方法声明上完全相同的一个方法。

为了满足里氏替换原则,重写必须有以下两个限制:

  • 子类方法的访问权限不能低于父类方法
  • 子类方法的返回类型必须是父类方法返回类型的原类型或其子类型。

使用 @Overwrite 注解可以通过编译器来检查这两个限制是否满足。

  1. 重载(overload)

存在与同一个类中,指多个方法的名称相同,但是参数类型、个数、顺序至少有一个不同。

如果仅返回值不同,其他都相同,不能认为是重载。

Object 通用方法

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {}

equals

  1. 等价关系

    • 自反性:x.equals(x) 为 true
    • 对称性:x.equals(y) == y.equals(x) 为 true
    • 传递性:x.equals(y) && y.equals(z)x.equals(z) 为 true
    • 一致性:多次调用 equals 方法的结果不会变化
    • 与 null 比较:任何不为 null 的对象 x 调用 x.equals(null) 均为 false
  2. equals 与 ==

    • 对于基本类型,== 判断值是否相等,基本类型没有 equals 方法
    • 对于引用类型,== 判断两变量的引用是否相等,而 equals 则判断引用的对象是否等价
  3. 实现

    • 检查是否为同一个对象的引用,如果是则直接返回 true
    • 检查是否为同一个类型,不同直接返回 false
    • 将 Object 转型
    • 判断每个关键域是否相等
public class EqualExample {
    private int x;
    private int y;
    private int z;

    public EqualExample(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}

hashCode

hashCode 返回哈希值,而 equals 用于判断两个对象是否等价。等价的两个对象的哈希值也一定相等,但是哈希值相等的两个对象不一定等价。

在重写 equals 方法时应当总是重写 hashCode 以确保等价对象的哈希值也一定相等。

理想的哈希函数应该具有均匀性,不相等的对象应该均匀分布到所有可能的哈希值上。这就要求哈希函数能把所有域的值都考虑进来,可以将每个域都当做 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于左移一位。

一个数与 31 相乘可以转换成移位和减法操作:31*X == (X<<5)-X,编译器会自动执行该优化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

toString

默认返回的是对象的内存地址,如 ToStringExample@4554617c,其中 @ 符号后面的值为散列码的无符号十六进制表示。

Clone

  1. cloneable

clone() 是 Object 对象的 protected 方法,并非 public,如果一个类不显式的重新 clone 方法,其他类就无法直接去调用该类实例的 clone 方法。

public class CloneExample {
  private int a;
  private int b;
  
  @Override
  protected CloneExample clone() throws CloneNotSupportedException {
    return (CloneExample)super.clone();
  }
}

CloneExample e1 = new CloneExample();
CloneExample e2 = e1.clone(); // will throw: CloneNotSupportedException

直接调用重写后的 clone 方法会抛出以上异常,这是因为 CloneExample 并未实现 Cloneable 接口。

注意,clone 并非 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 只是规定了:如果一个类没有实现 Cloneable 接口,直接调用 clone 方法时就会抛出该异常。

  1. 浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象。

// 像上面一样实现 Cloneable 接口并重写 clone 方法
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = e1.clone();

e2.setValue(11);
assert e1.getValue() == 11; // true
  1. 深拷贝

拷贝对象和原始对象的引用类型引用不同对象。

public class DeepCloneExample implements Cloneable {
  private int[] arr;
  
  @Override
  protected DeepCloneExample clone() throws CloneNotSupportException {
    DeepCloneExample cloned = (DeepCloneExample) super.clone();
    result.arr = new int[arr.length];
    for(int i=0;i<arr.length;i++){
      cloned.arr[i]=arr[i];
    }
    return cloned;
  }
}
  1. clone 的替代方案

使用 clone 方法来拷贝对象既复杂又有风险,可能会抛出异常,并且还需要类型转换。可以基于 Effective Java 中的建议,通过拷贝构造函数或拷贝工厂来拷贝一个对象。

public class CloneConstructorExample {
    private int[] arr;

    public CloneConstructorExample() {
        arr = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i;
        }
    }

    public CloneConstructorExample(CloneConstructorExample original) {
        arr = new int[original.arr.length];
        for (int i = 0; i < original.arr.length; i++) {
            arr[i] = original.arr[i];
        }
    }

    public void set(int index, int value) {
        arr[index] = value;
    }

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

关键字

final

  1. 变量

声明数据为常量,可以为编译时常量,也可以是在运行时被初始化后不能修改的常量。

  • 对于基本类型,fianl 使数值不变
  • 对于引用类型,final 使引用不变,即不能被重新赋值为别的对象,但是所引用对象的值可以被修改
  1. 方法

声明的方法不能被子类重写。

被 private 修饰的方法隐式的被指定为 fianl,如果在子类中定义的方法和基类中某个 private 方法签名一样,此时子类的方法并被重写基类方法,而是在子类中重新定义了一个新的方法。

声明类不能被继承。

static

  1. 静态变量

静态变量:又称为类变量,该变量属于类,而非该类的实例,类的所有实例都共享该变量,并通过类名来引用该变量,在内存中仅有一份。

实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。

  1. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须要实现,也就是说他不能抽象方法。

只能访问所属类的静态字段(变量)和静态方法,方法中不能有 this 和 super 关键字。

  1. 静态语句块

静态语句块将在类初始化时执行一次。

  1. 静态内部类

非静态内部类属于外部类的实例,而静态内部类属于外部类本身,通过外部类来引用该静态内部类。

  1. 静态导包

通过静态导包,可以在使用静态变量和方法时不用再逐个指明 ClassName,可一件简化代码:

import static com.xxx.ClassName.*;
  1. 初始化顺序

静态变量和语句块优先于实例变量和普通语句块,它们的顺序由代码中的编写顺序决定。然后才是构造函数。

存在集成的情况下,初始化顺序为:

  • 父类(静态变量、静态语句块)
  • 子类(静态变量、静态语句块)
  • 父类(实例变量、普通语句块)
  • 父类(构造函数)
  • 子类(实例变量、普通语句块)
  • 子类(构造函数)

反射

每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 class 文件,该文件内存保存着 Class 对象。

类加载相当于 Class 对象的加载。类在第一次使用时才会动态加载到 JVM 中,或者使用 Class.forName(...) 主动加载一个类,该方法会返回一个 Class 对象。

反射可以在运行时提供类的信息,并且该类可以在运行时才被加载进来,甚至在编译期该类的 class 尚不存在。

Class 和 java.lang.reflect 一起为反射提供了支持,java.lang.reflect 类库主要包括以下三个类:

  • Field:可以使用 get 和 set 方法读取或修改 Field 对象关联的字段。
  • Method:可以使用 invoke 方法滴啊用与 Method 对象关联的方法。
  • Constructor:可以使用 Constructor 创建新的对象。

高级用法:

  • Extensibility Features : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.

  • Class Browsers and Visual Development Environments : A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.

  • Debuggers and Test Tools : Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.

反射的缺点:

反射虽然强大但不应被直接使用。如果能够在不使用反射的情况下完成操作,则应避免使用反射。使用反射时应注意以下几点:

  • Performance Overhead : Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
  • Security Restrictions : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
  • Exposure of Internals :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.

异常

Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,Exception 分为两种:

  • 受检异常:需要 try…catch 语句捕获并进行处理,并且可以从异常中恢复。
  • 非受检异常:程序运行时错误,比如除零操作会引起 ArithmeticException,这时程序崩溃且无法恢复。
NAME

泛型

注解

Java 注解是附加在代码中的一些元信息,用于一些工具在编译期、运行时执行解析并使用,起到说明、配置的功能。注解不会也不应该影响代码的实际逻辑,仅仅起到辅助性的作用。

特性

版本特性

Java 8

  • Lambda Expressions
  • Pipelines and Streams
  • Date and Time API
  • Default Methods
  • Type Annotations
  • Nashhorn JavaScript Engine
  • Concurrent Accumulators
  • Parallel operations
  • PermGen Error Removed

Java 7

  • Strings in Switch Statement

  • Type Inference for Generic Instance Creation

  • Multiple Exception Handling

  • Support for Dynamic Languages

  • Try with Resources

  • Java nio Package

  • Binary Literals, Underscore in literals

  • Diamond Syntax

与 C++

  • Java 是纯粹的面向对象语言,所有的对象都继承自 Object,C++ 为了兼容 C 同时支持面向对象和面向过程。
  • Java 通过虚拟机实现跨平台特性,但是 C++ 依赖于特定的平台。
  • Java 没有指针,其引用可以理解为安全指针,而 C++ 具有与 C 一样的指针。
  • Java 支持自动垃圾回收,而 C++ 需要手动回收。
  • Java 不支持多重继承,只能通过实现多个接口到达相同的目的,而 C++ 支持多重继承。
  • Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是是语言内置的操作,不属于操作符重载,而 C++ 可以。
  • Java 的 goto 是保留字,不可以使用,C++ 可以使用。
  • Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理制冷可以实现条件编译。

JRE 与 JDK

  • JRE 是 JVM 程序,Java 应用需要运行在 JRE 之上。
  • JDK 是 JRE 的超集,带有 JRE 和用于编写 Java 程序的工具,比如 javac。

参考资料