Java

💩

基本程序设计结构

  • 变量:
    Java语言支持的变量类型有:
      1. 类变量:独立于方法之外的变量,用static修饰  
      2. 实例变量:独立于方法之外的变量,不过没有static修饰  
      3. 局部变量:类的方法中的变量  
    
    实例:
    1
    2
    3
    4
    5
    6
    7
    public class Variable {
    static int allClicks = 0; //类变量
    String str = "hello world"; //实例变量
    public void method() {
    int i = 0; //局部变量
    }
    }

类和对象

  • Java中的受保护部分对于所有子类及同一个包中的所有其他类都可见。

  • protected需要从以下两个点来分析说明:

    1. 子类与基类在同一个包中:被声明为protected的变量、方法和构造器能被同一个包中的任何其他类访问;
    2. 子类与基类不在同一个包中:那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected方法。
  • final

    • 在Java中,用关键字final指示常量。
    • 关键字final表示这个变量只能被赋值一次。一旦被赋值之后,就不能够再更改了。习惯上,常量名使用全大写。
    • 在Java中,经常希望某个变量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字static final设置一个类常量。
    • final关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写。(注:实例变量也可以被定义为final,被定义为final的变量不能被修改。被声明为final类的方法自动得声明为final,但是实例变量并不是final(如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域))
  • 构建字符串:StringBuilder

    1. 构建一个空的字符串构建器:
      1
      StringBuilder builder = new StringBuilder();
    2. 当每次需要添加一部分内容时,就调用append()方法:
      1
      2
      builder.append(ch); //appends a single character
      builder.append(str); //appends a string
    3. 在需要构建字符串时就调用toString()方法,将可以得到一个String对象,其中包括了构建器中的字符序列:
      1
      String completedString = builder.toString();
  • 一旦创建了数组,就不能改变它的大小(尽管可以改变每一个数组元素)。如果经常需要在运行过程中扩展数组的大小,就应该使用另一种数据结构 -- 数组列表。

  • 初始化数据域的方法:

    1. 在构造器中设置值
    2. 在声明中赋值
    3. 初始化块(首先运行初始化块,然后才运行构造器的主体部分,这种机制不是必需的,也不常见,通常,直接将初始化代码放在构造器中)
  • 在对象与对象变量之间存在着一个重要的区别。例如,语句 Date deadline;
    定义了一个对象变量deadline,它可以引用Date类型的对象,但是,一定要认识到:变量deadline不是一个对象,实际上也没有引用对象。此时,不能将任何Date方法应用于这个变量上。语句 s = deadline.toString(); 将产生编译错误。必须首先初始化变量deadline,这里有两个选择,可以用新构造的对象初始化这个变量:deadline = new Date(); 也可以让这个变量引用一个已存在的对象:deadline = birthday;

  • 一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅是引用了一个对象。在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new操作符的返回值也是一个引用。

  • 可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象。deadline = null;

  • 局部变量不会自动地初始化为null,而必须通过调用new或将他们设置为null进行初始化。

  • 所有的Java对象都存储在堆中。

  • 在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。

  • 构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。

  • 注意不要编写返回引用可变对象的访问器方法。如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。如果需要返回一个可变数据域的拷贝,就应该使用clone。对象的克隆是指存放在另一个位置上的对象副本。修改前的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    class Employee {
    private Date hireDay;
    ...
    public Date getHireDay() {
    return hireDay;
    }
    ...
    }

    修改后的代码:

    1
    2
    3
    4
    5
    6
    7
    class Employee {
    ...
    public Date getHireDay() {
    return hireDay.clone();
    }
    ...
    }
  • static游离块、一般游离块、子父类构造方法,执行顺序:父类静态游离块 > 子类静态游离块 > 父类游离块 > 父类构造函数 > 子类游离块 > 子类构造函数

  • 块作用域:不能在嵌套的两个块中声明同名的变量。但是在C++中,可以在嵌套的块中重定义一个变量,在内层定义的变量会覆盖在外层定义的变量。

  • 一个方法可以访问所属类的所有对象的私有数据。

静态域和静态方法

  • 静态域:
    如果将域定义为static,每个类中只有一个这样的域,而每一个对象对于所有的实例域却都有自己的一份拷贝。例如,假定需要给每一个雇员赋予唯一的标识码。这里给Employee类添加一个实例域id和一个静态域nextId:

    1
    2
    3
    4
    5
    class {
    private static int nextId = 1;
    private int id;
    ...
    }

    现在,每一个雇员对象都有一个自己的id域,但这个类的所有实例将共享一个nextId域。换句话说,如果有1000个Employee类的对象,则有1000个实例域id。但是,只有一个静态域nextId。即使没有一个雇员对象,静态域nextId也存在。它属于类,而不属于任何独立的对象。

  • 在下面两种情况下使用静态方法:

    1. 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。
    2. 一个方法只需要访问类的静态域(例如:Employee.getNextId)。
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。

  • 静态方法只能访问静态成员,实例方法可以访问静态和实例成员。

  • 之所以不允许静态方法访问实例成员变量,是因为实例成员变量是属于某个对象的,而静态方法在执行时,并不一定存在对象。

方法参数

  • 方法参数共有两种类型:

    1. 基本数据类型(数字、布尔值)
    2. 对象引用
  • 方法参数的使用情况:

    1. 一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)。
    2. 一个方法可以改变一个对象参数的状态。(实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。)
    3. 一个方法不能让对象参数引用一个新的对象。
  • 调用:
    按值调用:表示方法接收的是调用者提供的值。
    按引用调用:表示方法接收的是调用者提供的变量地址。
    一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
    Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。
    Java程序设计语言对对象采用的不是引用调用,实际上,对象引用进行的是值传递。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    public class ParamTest{
    public static void main(String[] args){

    /* Test1: Methods can't modify numeric parameters*/
    //一个方法不能修改一个基本数据类型的参数(即数值型和布尔型)
    System.out.println("Testing tripleValue:");
    double percent = 10;
    System.out.println("Before: percent = " + percent);
    tripleValue(percent);
    System.out.println("After: percent = " + percent);

    /* Test2: Methods can change the state of object parameters*/
    //一个方法可以改变一个对象参数的状态
    System.out.println("\nTesting tripleValue:");
    Employee harry = new Employee("Harry",500000);
    System.out.println("Before: salary = " + harry.getSalary());
    tripleSalary(harry);
    System.out.println("After: salary = " + harry.getSalary());

    /* Test3: Methods can't attach new objects to object parameters*/
    //一个方法不能让对象参数引用一个新的对象
    System.out.println("\nTesting swap:");
    Employee a = new Employee("Alice", 70000);
    Employee b = new Employee("Bob", 60000);
    System.out.println("Before: a = " + a.getName());
    System.out.println("Before: b = " + b.getName());
    swap(a,b);
    System.out.println("After: a = " + a.getName());
    System.out.println("After: b = " + b.getName());
    }

    public static void tripleValue(double x){ //doesn't work
    x = 3 * x;
    System.out.println("End of method: x = " + x);
    }

    public static void tripleSalary(Employee x){ //works
    x.raiseSalary(200);
    System.out.println("End of method: salary = " + x.getSalary());
    }

    public static void swap(Employee x, Employee y){
    Employee temp = x;
    x = y;
    y = temp;
    System.out.println("End of method: x = " + x.getName());
    System.out.println("End of method: y = " + y.getName());
    }
    }

    class Employee{ //simplified Employee class
    private String name;
    private double salary;

    public Employee(String n, double s){
    name = n;
    salary = s;
    }

    public String getName(){
    return name;
    }

    public double getSalary(){
    return salary;
    }

    public void raiseSalary(double byPercent){
    double raise = salary * byPercent / 100;
    salary += raise;
    }
    }

继承

  • 在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。然而,超类中的有些方法对子类Manager并不一定适用。具体来说,Manager类中的getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法。
    那么如何实现这个方法呢?乍看起来似乎很简单,只要返回salary和bonus域的总和就可以了:

    1
    2
    3
    public double getSalary() {
    return salary + bonus; // won't work
    }
  • 然而,这个方法并不能运行。这是因为Manger类的getSalary方法不能地访问超类的私有域。也就是说,尽管每一个Manager对象都拥有一个名为salary的域,但在Manager类的getSalary方法中并不能直接地访问salary域。只有Employee类的方法才能够访问私有部分。如果Manager类的方法一定要访问私有域,就必须借助于公有的接口,Employee类中的公有方法getSalary正是这样一个接口。

  • 现在,再试一下。将对salary域的访问替换成调用getSalary方法。

    1
    2
    3
    4
    public double getSalary() {
    double baseSalary = getSalary(); // still won't work
    return baseSalary + bonus;
    }

    上面这段代码仍然不能访问。问题出现在调用getSalary的语句上,这是因为Manager类也有一个getSalary方法(就是正在实现的这个方法),所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。

  • 这里需要指出:我们希望调用超类Employee中的getSalary方法,而不是当前类的这个方法。为此,可以使用特定的关键字super解决这个问题:

    1
    2
    3
    4
    public double getSalary() {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
    }
  • 在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。

  • super在构造器中的应用:

    1
    2
    3
    4
    public Manager(String n, double s, int year, int month, int day) {
    super(n,s,year,month,day);
    bonus = 0;
    }

    这里的关键字super具有不同的含义。语句super(n,s,year,month,day);是“调用超类Employee中含有n、s、year、month和day参数的构造器”的简写形式。
    由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。
    如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用其他构造器,则Java编译器将报告错误。(子类不能继承父类的构造器(构造方法或者构造函数),如果父类的构造器带有参数,则必须在子类的构造器中显式地通过super关键字调用父类的构造器并配以适当的参数列表。如果父类构造器没有参数,则在子类的构造器中不需要使用super关键字调用父类构造器,系统会自动调用父类的无参构造器。)

  • 关键字this的两个用途:

    1. 引用隐式参数
    2. 调用该类其他的构造器
  • 关键字super的两个用途:

    1. 调用超类方法
    2. 调用超类的构造器
  • 重写(override):

    • 声明为final的方法不能被重写
    • 声明为static的方法不能被重写,但是能够被再次声明
    • 构造方法不能被重写
    • 如果不能继承一个方法,则不能重写这个方法
  • 重载(overload):

    • 被重载的方法必须改变参数列表(参数个数或类型不一样)
    • 被重载的方法可以改变返回类型
    • 被重载的方法可以改变访问修饰符
    • 被重载的方法可以声明新的或更广的检查异常
    • 方法能够在同一个类中或者在一个子类中被重载
  • 一个对象变量可以指示多种实际类型的现象被称为多态。在运行时能够自动地选择调用哪个方法的现象称为动态绑定。

  • 多态的实现方法:重写、接口、抽象类和抽象方法。

  • 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定。

  • 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。

  • 如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为内联。如果方法很简短、被频繁调用且没有真正地被覆盖,那么虚拟机中的即时编译器就会将这个方法进行内联处理。

  • 强制类型转换:在将超类转换成子类之前,应该使用instanceof进行检查。

  • abstract关键字:

    1. 包含一个或多个抽象方法的类本身必须声明为抽象的。
    2. 除抽象方法之外,抽象类还可以包含具体数据和具体方法。
    3. 抽象方法充当着占位的角色,它们的具体实现在子类中。
    4. 扩展抽象类可以有两种选择。一种是在子类中定义部分抽象方法或者抽象方法也不定义,这样就必须将子类也标记为抽象类;另一种是定义全部抽象方法,这样一来,子类就不是抽象的了。
    5. 类即使不含抽象方法,也可以将类声明为抽象类。
    6. 抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。(抽象类不能直接通过new去实例化一个对象,那它就是不能实例化,要获取抽象类的对象,需要先用一个类继承抽象类,然后去实例化子类。也可以用匿名内部类,在抽象类中创建一个匿名的子类,继承抽象类,通过特殊的语法实例化子类的对象。)
    7. 可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。
    8. 构造方法、类方法(用static修饰的方法)不能声明为抽象方法。

接口和内部类

  • 接口绝不能含有实例域,也不能在接口中实现方法。提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实例域的抽象类。

  • 接口不是类,尤其不能使用new运算符实例化一个接口:

    1
    x = new Compare(...); //ERROR

    然而,尽管不能构造接口的对象,却能声明接口的变量:

    1
    Compare x; //OK

    接口变量必须引用实现了接口的类对象:

    1
    x = new Employee(...); //OK provided Employee implements Comparable

    接下来,如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口:

    1
    if (anObject instanceof Comparable) {...}

    与可以建立类的继承关系一样,接口也可以被扩展。这里允许存在多条从具有较高通用性的接口到较高专用性的接口的链。例如,假设有一个称为Moveable的接口:

    1
    2
    3
    public interface Moveable {
    void move (double x, double y);
    }

    然后,可以以它为基础扩展一个叫做Powered的接口:

    1
    2
    3
    public interface Powered extends Moveable {
    double milePerGallon();
    }

    虽然在接口中不能包含实例域或静态方法,但却可以包含常量。
    与接口中的方法都自动地被设置为public一样,接口中的域将被自动设为public static final。

  • 为什么要使用内部类?

    1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据。
    2. 内部类可以对同一个包中的其他类隐藏起来。
    3. 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
  • Java内部类还有另外一个功能,这使得它比C++的嵌套类更加丰富,用途更加广泛。内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。

  • 局部内部类:
    局部内部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。

  • 由外部方法访问final变量:
    局部类不仅能够访问包含它们的外部类,还可以访问局部变量,不过,这些局部变量必须被声明为final。

  • final关键字可以应用于局部变量、实例变量和静态变量。在所有这些情况下,它们的含义都是:在创建这个变量之后,只能够为之赋值一次。此后,再也不能修改它的值了,这就是final。不过,在定义final变量的时候,不必进行初始化。

  • 匿名内部类:

    1. 如果构造器参数的闭圆括号跟一个开花括号,正在定义的就是匿名内部类。
    2. 由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类的构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。
  • 静态内部类:
    有时候,使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。

异常

  • Throwable

    • Error(未检查异常)
    • Exception
      • IOException(已检查异常)
      • RuntimeException(未检查异常)
  • 声明:方法应该在其首部声明所有可能抛出的异常。这样可以从首部反映出这个方法可能抛出哪类已检查异常。但是,不需要声明Java的内部错误,即从Error继承的错误。同样,也不应该声明从RuntimeException继承的那些未检查异常。总之,一个方法必须声明所有可能抛出的已检查异常,而未检查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。

  • 由于程序错误导致的异常属于RuntimeException,而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
    派生于RuntimeException的异常包含下面几种情况:

    1. 错误的类型转换
    2. 数组访问越界
    3. 访问空指针

    不是派生于RuntimeException的异常包括:

    1. 试图在文件尾部后面读取数据
    2. 试图打开一个不存在的文件
    3. 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在

    “如果出现RuntimeException异常,那么就一定是你的问题”是一条相当有道理的规格。

  • 如果在子类中覆盖了超类的一个方法,子类方法中声明的已检查异常不能比超类方法中声明的异常更通用(也就是说,子类方法中可以抛出更特定的异常,或者根本不抛出任何异常)。特别需要说明的是,如果超类方法没有抛出任何已检查异常,子类也不能抛出任何已检查异常。

  • 如果想传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。

泛型

  • 对于一个static方法而言,无法访问泛型类型的参数。如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法。

  • 泛型类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Pair<T> {
    private T first;
    private T second;

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

    public T getFirst() {return first;}
    public T getSecond() {return second;}

    public void setFirst(T newValue) {first = newValue;}
    public void setSecond(T newValue) {second = newValue;}
    }
  • 泛型方法:

    1
    2
    3
    4
    5
    class ArrayAlg {
    public static <T> T getMiddle(T... a) {
    return a[a.length / 2];
    }
    }

    类型变量放在修饰符(这里是public static)的后面,返回类型的前面。
    泛型方法可以定义在普通类中,也可以定义在泛型类中。
    当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

    1
    String middle = ArrayAlg.<String>getMiddle("John","Q","Public");

    大多数情况下,方法调用中可以省略类型参数。编译器有足够信息能够推断出所调用的方法。也就是说,可以调用:

    1
    String middle = ArrayAlg.getMiddle("John","Q","Public");

多线程

  • 线程可以有如下6种状态:New(新创建)、Runnable(可运行)、Blocked(被阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(被终止)。

    • 在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行。
    • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。
    • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。
    • 有几个方法有一个超时参数,调用它们导致线程进入计时等待状态。
    • 线程因如下两个原因之一而被终止:因为run方法正常退出而自然死亡;因为一个没有捕获的异常终止了run方法而意外死亡。
  • 同步:

    1. 锁和条件的关键之处:
      • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
      • 锁可以管理试图进入被保护代码段的线程。
      • 锁可以拥有一个或多个相关的条件对象。
      • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
    2. synchronize、wait()、notifyAll()
    3. 除了调用同步方法获得锁,线程还可以通过进入一个同步阻塞获得锁:
      1
      2
      3
      synchronize(obj) {
      critical section
      }
    4. volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。(volatile修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。)