Java核心技术 卷I:基础知识

赵高雅
2023-12-01

第一章 Java程序设计概述

太简单了,直接略过。

1.2 Java“白皮书”的关键术语

  1. 简单性:指相对于C++简单(指针、多重继承等),但设计者也并没有试图清楚C++中所有不适当的特性
  2. 面向对象:java与C++主要不同在于多重集成,以及接口概念
  3. 网络技能
  4. 健壮性
  5. 安全性
  6. 体系结构中立
  7. 可移植性
  8. 解释性:过去Java解释器可以在任何移植了解释器的机器上执行java字节码,现在使用即使编译器将字节码再翻译成机器码
  9. 高性能
  10. 多线程
  11. 动态性

第二章 Java程序设计环境

我选择使用了JetBrainde IDEA社区版,直接忽略

第三章 Java的基本程序设计结构

3.3 数据类型

主要关心的是boolean类型,包含false和true,与C++是同一个类型的

3.4 变量

合法特殊字符为任何代表字母的Unicode字符,但是不能出现+(操作符)和copyright(除字母外的其他Unicode字符)。其中$尽量不要在自己的代码中使用,一般出现在Java编译器或者其他工具生成的名字中。

需要显示初始化变量,与C++一致。

在java中,使用final指示常量,如final double CM_PER_INC=2.54。final表示该变量只能赋值一次,一旦赋值就不能修改,习惯上常量名使用大写。

类常量可以使用static final进行设定,在某个类内部定义

public class test{
    public static final double CM_PER_INC=2.54;
}

3.5 运算符

求余符号为%

3.5.6 强制类型转换

double x = 9,997;
int nx = (int) x;

3.6 字符串

任何语言的字符串都是值得重视的。

java使用类型String定义字符串

String greeting = "Hello";
String s = greeting.substring(0,3);//Hel 前闭后开区间,对于substring(a,b),子串长度为b-a
String t = "123"+"456";//123456

3.6.3 不可变字符串

String类不能直接修改字符串,只能够使用子串+拼接的方式进行间接修改。

原理是编译器让所有的字符串共享,可以想象字符串被放在公用的存储池中,赋值字符串后,新字符串与原字符串指向相同的对象。

3.6.4 检测字符串是否相等

可以使用s.equals(t)检测是否相等,但不能使用==,后者只能确定两个字符串是否放在同一个位置上,这点C++应该也是一样的。

3.6.5

可以调用string.length()返回长度

可以调用string.charAt(index)返回指定位置的char类型变量。

3.6.7 字符串API

……不用想着背了,肯定是用到的时候再查的。

3.7 输入输出

IO也是很重要的

标准输出流:System.out.println

标准输入流:

import java.util.*;//Scanner类定义在java.util包中
Scanner in = new Scanner(System.in);//Scanner绑定标准输入流
String nextline = in.nextLine();//读取下一行
String nextword = in.next();//读取下一个单词
int age = in.nextInt();//读取整数

格式化输出:System.out.printf("%8.2f",x);,同样,与C++类似

3.7.3 文件输入输出

也是一般最常用的

Scanner in = new Scanner(Paths.get("myfile.txt"));//读取文件
PrintWriter out = new PrintWriter("myfile.txt");//写文件

相对路径为启动环境的根目录。使用集成开发环境时,路径地址可以使用String dir = System.getProperty("user.dir");获得

3.8 控制流程

3.8.1 块作用域

block,指花括号括起来的若干条简单java语句

java支持带标签的break来跳出多重循环,标签必须放在希望跳出的最外层循环之前,加上引号


read_data:
while(...){
    for(...){
        break read_data;//直接跳到指定循环之外
    }
}

3.9 大数值

java.math中的BigInteger和BigDecimal,可以处理任意长度数字序列的树枝。前者为任意精度整数运算,后者为任意精度浮点数运算。可以使用valueOf方法将普通数值转换为大数值。

3.10 数组

int[] a = new int[100],声明数组

匿名数组smallPrimes = new int[]{1,2,3,4,5},可以在不创建新变量的情况下重新初始化一个数组

数组拷贝int[] luckyNumbers = smallPrimes;为引用,想要拷贝数值应该使用Arrays.copyOf方法(可以使用该方法来增加数组长度)

数组排序Arrays.sort(arr)

3.10.7 不规则数组

double[][] balances = new double[YEARS][RATES]

Java本质上没有多维数组,只有一维数组。因此,二维数组的每一行可以拥有不同的长度

int[][] odds = new int[NMAX+1][]
for(int n=0;n<=NMAX;n++){
    odds[n] = new int[n+1];
}

第4章 对象与类

类之间的关系:

  • 依赖"use a"。如果一个类的方法操纵另一个类,就说一个类依赖于另一个类。应该尽可能降低互相依赖的类的数量
  • 聚合"has a"。一个类的对象包含另一个类,即为聚合
  • 继承"is a"。

空对象null

4.2.3 更改器方法与访问器方法

GregorianCalendar now = new GregorianCalendar();
int month = now.get(Calendasr.MONTH);//访问器
deadline.set(Calendar.YEAR,2001);//更改器

对实例域作出修改的访问成为更改器方法,仅访问实例域的方法称为访问器方法。

4.3 用户自定类

类方法构成

public String getName()

其中public表示访问控制,String表示返回值,函数名内部为形参表

4.3.2 多个源文件的使用

java编译器可以认为内置了make功能,就算使用java XXX.java命令没有显示编译其他的java文件,它也会查找其他的java文件。

4.3.3 剖析

一般建议实例域采用private来维持封装

4.3.4 构造器

C++中的构造函数,没有看到有什么不同的

PS:Java中的所有对象都是在堆中构造的,容易遗漏new操作符
PPS:不要在构造器中定义与实例域重名的局部变量,会重复。

4.3.5 隐式参数

即对于类方法

public void raiseSalary(double byPercent){
    double raise = salary*byPercent/10;
    salary += raise;
    //一般推荐this.salary += raise;,这样可以显式区分局部变量和实例域
}

java中可以选择显示调用this指针,也可以不调用。这里类方法中的第一个参数为隐式参数,即类自己。该类方法的副作用就是salary会一起改变。

方法可以访问所有类的私有域(与C++类似,抱歉我C++学的不好)

class Employee{
    private String name;
    public boolean equals(Employee other){
        return name.equals(other.name);
    }
}

4.4 静态域

对象中的变量一般是跟着对象走的,但是static的变量可以看作独立于具体对象之外。这样,对于所有的对象,它们共享同样的静态域。

4.4.3 静态方法

静态方法是一种不能向对象实施操作的方法,可以认为静态方法没有this参数。

可以使用静态方法来实现工厂函数。

4.5 方法参数

一般来说,存在按值调用和按引用调用。Java总是默认采用按值调用,但是需要注意,=赋值号一般总是直接复制对象的地址,除非使用clone

这也就是说,方法得到的是所有参数值的一个拷贝。但是如果参数是自定类的话,则拷贝的内容为类的地址,因此可以认为是引用传值。

4.6 对象构造

4.6.1 重载

即构造函数重载,相同的构造函数可以使用相同的名字、不同的参数。

4.6.6 调用另一个构造器

关键字this引用方法的隐式参数

public Employee(double s){
    //call Employee(String,double)
    this("Employee #"+nextId,s);
    nextid++;
}

这样对于公共的构造器代码,只用编写一次即可。

4.6.8 对象析构

为类添加finalize方法,将在垃圾回收器清除对象之前调用。

PS:在实际应用中不要依赖finalize方法,因为实际很难知道具体什么时间会调用这个函数

4.7 包

Java允许使用包将类组织起来,有点类似C++中的namespace

4.7.1 类的导入

导入的方式,一种是输入全名java.util.Date today = new java.util.Date();

也可以使用import java.util.*;导入全部类,这样就不用写入全名了。

另外,*只能导入一个包,不能使用java.*的方式导入所有包。

4.7.2 静态导入

import static java.lang.System.*可以导入静态方法和静态域。

4.7.3 将类放入包中

想要将类放入包中,就必须将包的名字放在源文件的开头,可以必须写全名,例如
package com.horstmann.corejava;,而不能只是package corejava

同时,包中的文件需要被放置在与完整的包名匹配的子目录中,例如上面的包应该被放在com/horstmann/corejava下。

4.7.4 包作用域

对于private定义的类,只有同一个包能够访问。而public类则是导入包即可见。

4.8 类路径

主要是使用JAR文件。一般将JAR文件放在一个目录中,然后设置类路径classpath(一般是java -classpath/-cp XXX),就可以读取。在UNIX环境中,类路径的不同项目之间采用冒号分割。也可以设置环境变量CLASSPATH

4.9 文档注释

JDK的工具,javadoc,可以由源文件生成一个HTML文档。

有点类似python的doc,不过是放在定义的前面而不是后面,并且对于/**开头的注释来说,每一行的开头都要有*

方法注释还可以使用以下的标记:

  • @param变量描述
  • @return 返回描述
  • @throws 类描述

通用注释:

  • @author姓名
  • @version版本文本
  • @since 对引入特性的版本描述

执行命令javadoc -d docDirectory nameOfPackage,即可生成docDirectory下的指定包的HTML文件。

4.10 类设计技巧

  1. 一定要保证数据私有,绝对不要破坏封装。我觉得在这里主要是因为JAVA的语言特性,对象默认传递地址使得一旦发生修改,查找起来会非常痛苦。
  2. 一定要对数据初始化
  3. 不要在类中使用过多的基本类型(便于理解)
  4. 不是所有的域都需要独立的域访问器和域更改器。
  5. 将职责过多的类进行分解
  6. 类名和方法名要能体现它们的职责。(PS:个人观点,不要出现magic number)

第5章 继承

5.1 类、超类和子类

可以使用关键词extends表示继承,且JAVA中只有公有继承,没有C++中的私有继承和保护继承

一些显然但容易忘的事实:子类方法并不能直接访问超类的私有域,必须借助于公有接口。如果需要调用超类的同名方法,应该使用特定关键字super

同时,super可以在构造器中使用,比如

public Manager(String test){
    super(test);
}

super与this指针具有类似之处,它们都能调用方法和构造器。但是super是不能赋值的,它只能指示编译器。

5.1.2 多态

比如Manager继承了Employee,显然Employee变量即可以是Employee对象,也可以是Manager对象或者其他继承对象。

5.1.3 动态绑定

多态的特征依赖于编译器调用对象方法的执行过程:

  1. 编译器查看对象的声明类型和方法名。编译器会遍历所有同名方法,列举所有同名方法
  2. 编译器将查看调用方法时提供的参数类型,即重载解析。至此,编译器获得需要调用的方法名字和参数类型。
  3. 对于private方法、static方法和final方法,编译器可以唯一的确认调用。这种称为静态绑定。
  4. 与之对应,调用的方法依赖于隐式参数(this)的实际类型,这种方式称为动态绑定。

5.1.4 阻止继承

不允许扩展的类称为final类

类中特定方法也可以声明为final,这样子类就不能覆盖这个方法。(final类中的所有方法自动称为final方法)

这样做的意义是为了保证它们在子类中不会改变语义。

5.1.5 强制类型转换

Manager boss = (Manager) staff[0];,其中staff可能是继承类数组。如果staff中是超类,则会报错。

可以使用

if (staff[1] instanceof Manager){
    boss = (Manager) staff[1]
}

确保staff中存储的是Manager或其子类。
PS:null instanceof C不会产生异常,只会返回false。

5.1.6 抽象类

对于上层的通用类,可能设计成抽象类会更好。

abstract class Person{
    public abstract String getDescription()
}

抽象类可以不用实现具体方法,但是可以包含具体数据和具体方法。
PS:很多人认为,在抽象类中包含具体方法是有害的。、

5.1.7

如果希望超类中的某些部分被子类访问,应该设为protected而非private

但是这样只能访问自己对象的超类中的指定部分,而不能访问其他对象的超类中的指定部分。这与private还是有一定区别的。

5.2 Object:所有类的超类

  • Object.equals(),判断一个对象是否等于另一个对象,即判断两者是否具有相同的引用。
  • Object.toString(),返回表示对象值的字符串。中间可以使用getClass().getName()获得类名的字符串。

5.3 泛型数组列表

Java允许在运行时确定数组的大小。

int actualSize = ...;
Employee[] staff = new Employee[actualSize];

当然,这样子也无法动态更改数组大小。因此,一般使用ArrayList类来进行实现。该类类似于C++中的vector

ArrayList<Employee> staff = new ArrayList<Employee>();

ArrayList只能使用get和set来访问数组元素。

  • size方法:返回数组列表中实际元素数量。
  • ensureCapacity(int capacity),提前分配足够空间
  • trimToSize方法,可以将存储区域的大小调整为所需要的存储空间数目。
  • add方法,在数组列表的尾端添加一个元素
  • toArray方法,可以将ArrayList的内容赋值给一个数组。
  • get(index)
  • set(index,ele)
  • remove(index) 删除一个元素,后面的元素向前移动,返回被删除的元素

5.4 对象包装器与自动装箱

比如尖括号内的类型不能是基础类型,所以必须写成ArrayList<Integer> list = new ArrayList<>();

此时list.add(3);会自动变换成list.add(Integer.valueOf(3));该过程成为自动装箱autoboxing,或者autowrapping

5.5 参数数量可变的方法

例如System.out.printf的实现public PrintStream printf(String fmt,Object... args){return format(fmt,args);}

其中...打死表这个方法可以接收任意数量的对象,后者args是一个Object[]数组。

5.6 枚举类

public enum Size{SMALL,MEDIUM,LARGE};

5.7 反射

能够分析类能力的程序称为反射

5.7.1 Class类

Object类中的getClass()方法可以返回一个Class类型的实例。虚拟机为每个类型管理一个Class对象,可以使用==运算符进行比较,比如

if (e.getClass() == Employee.class)

以及使用newInstance()来快速地创建一个类的实例e.getClass().newInstance(),可以创建一个与e具有相同类类型的实例,调用默认的构造器。

String s = "java.util.Date";
Object m = Class.forName(s).newInstance();//forName根据类名进行查找,所以创建了一个java.util.Date类型的变量

可以使用Constructor.newInstance(Object[] args)来调用指定的构造器

5.7.2 捕获异常

try{


}catch(Exception e){
    e.printStackTrace();
}

5.7.3 利用反射分析类的能力

反射机制最重要的内容——检查类的结构

java.lang.reflect包中有三个类:

  • Field,描述类的域
  • Method,描述类的方法
  • Constructor,描述类的构造器

Field有getType方法,返回描述域所属类型的Class对象。

5.7.4 在运行时使用反射分析对象

Employee harry = new Employee("Harry Hacker",35000,10,1,1989);
Class cl = harry.getClass();//拿到Employee对应的class对象
Field f = cl.getDeclaredField("name");//拿到Employee class的name作用域
Object v = f.get(harry);//harry的name field的具体值,即Harry Hacker

该代码有一个问题,name是私有域,因此会爆出错误。只有利用get方法才能得到可访问域的值。除非拥有访问权限,不然只能查看任意对象有哪些域,而不允许读取它们的值。

可以调用setAccessible方法来覆盖访问控制,这样就可以访问私有域了。

另一个问题是get方法返回的是Object,因此如果返回值是double的时候会有问题。此时应该使用getDouble方法,反射机制会自动打包。

泛用的toString()例子

package objectAnalyzer;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;

public class ObjectAnalyzer{
    private ArrayList<Object> visited = new ArrayList<>();

    /**
     * 将一个对象转为列举所有field的数组
     * @param obj an object
     * @return a string with the object's class name and all field names and value
     */
    public String toString(Object obj){
        if (obj == null) return "null";
        if (visited.contains(obj)) return "...";
        visited.add(obj);
        Class cl = obj.getClass();
        if (cl==String.class) return (String) obj;
        if (cl.isArray()){
            String r = cl.getComponentType() + "[]{";
            for(int i = 0;i<Array.getLegnth(obj);i++){
                if(i>0) r+=",";
                Object val = Array.get(obj,i);
                if(cl.getComponentType().isPrimitive()) r += val;
                else r += toString(val);
            }
            return r + "}";
        }

        String r = cl.getName();
        //inspect the fields of this class and all superclasses
        do{
            r += "[";
            Field[] fields = cl.getDeclaredFields();
            AccessibleObject.setAccessible(fields,true);
            //get the names and values of all fields
            for (Field f: fields){
                if(!Modifier.isStatic(f.getModifiers())){
                    if(!r.endsWith("[")) r += ",";
                    r += f.getName() + "=";
                    try{
                        Class t = f.getType();
                        Object val = f.get(obj);
                        if(t.isPrimitive()) r += val;
                        else r += toString(val);
                    }
                    catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
            r += "]";
            cl = cl.getSuperclass();
        }
        while(cl != null);

        return r;
    }
}

5.7.5 使用反射编写泛型数组代码

比如制作一个通用的Arrays.copyOf(array,legnth),这个方法可以用来扩展已经填满的数组。

第一次尝试:

public static Object[] badCopyOf(Object[] a,int newLength){
    Object[] newArray = new Object[newLength];
    System.arrayCopy(a,0,newArray,0,Math.min(a.legnth,newLength));
    return newArray;
}

问题在于,该函数返回的是对象数组Object[],这是基类数组。显然,将基类数组赋值给子类数组会报错。

为了编写通用数组代码,就必须创造与原数组类型相同的新数组,即要用到反射类。最关键的就是java.lang.reflect.Array.newInstance静态方法,它能够构造新数组,调用它时必须提供数组的元素类型和数组的长度。

public static Object goodCopyOf(Object a,int newLength){
    Class cl = a.getClass();//拿到对应的class对象
    if(!cl.isArray()) return null;
    Class componentType = cl.getComponentType();//拿到类型
    int length = Array.getLength(a);
    Object newArray = Array.newInstance(componentType,newLength);//创建数组
    System.arraycopy(a,0,newArray,0,Math.min(length,newLength));
    return newArray;
}

此时,CopyOf代码可以扩展任意数组,而不只是对象数组。这也是为什么将a设置为Object类型而非Object[]类型。

5.7.6 调用任意方法

表明上看,java没有方法指针,设计者认为接口是更好的方案。但是反射机制运行用户调用任何方法。

Field类的get方法查看对象域,而Method类有一个invoke方法,允许调用包装在当前Method对象中的方法。

Object invoke(Object obj,Object... args),第一个参数是隐式参数,其余对象提供显式参数。

对于静态方法,第一个参数可以忽略,设置为null

假设m1为Employee类的getName方法(String getName(void)),则String n = (String) m1.invoke(harry);就直接调用了这个方法。(PS:harry为对应的Employee对象)

得到Method对象可以使用Method getMethod(String name,Class.. parameterTypes),根据方法的名称和参数类型来得到想要的方法,例如Method m2 = Employee.class.getMethod("raiseSalary",double.class);

5.8 继承设计的技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现"is-a"关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
  6. 使用多态,而非类型信息
  7. 不要过多地使用反射

第6章 接口与内部类

6.1 接口

接口不是类,而是对类的一组需求描述,类要遵从接口描述的统一格式进行定义

例如:

public interface Comparable{
    int compareTo(Object other);
}

即要求任何实现Comparable接口的类都需要包含compareTo方法,并且具体事项都是符合的。

接口中的所有方法自动地属于public,因此不必提供关键字public

为了让类实现接口,一般包含两个步骤:

  1. 将类声明为实现给定的接口,类定义处仿照继承,implements interfaceName
  2. 对接口中的所有方法进行定义

PS:虽然接口并没有把方法声明为public,但是实现接口时,必须把方法声明为public。否则,编译器将认为这个方法的访问属性是包可见性,即类的默认访问属性,之后编译器就会给出试图提供更弱的访问权限的警告信息。

6.1.1 接口的特性

接口不是类,不能进行实例化,比如new等。到那时可以声明接口的变量,例如Comparable x。接口变量必须引用实现了接口的类对象,有点类似于严格限定的抽象基类。

类似地,也可以使用instanceof检查一个对象是否实现了某个特定的接口。anObject instanceof Comparable

接口之间可以继承:

public interface Movable{
    void move(double x,double y);
}
public interface Powered extends Movable{
    double milsPerGallon();
}

接口不能包含实例域或静态方法,但是可以包含常量:

public interface Powered extends Movable{
    double milsPerGallon();
    double SPEED_LIMIT = 95; // a public static final constant
}

接口中的域将被自动设为public static final

每个类只能有一个超类,但是可以实现多个接口.

6.1.2 接口与抽象类

使用抽象类表示通用属性时会存在一个问题:每个类只能扩展于一个类,这使得多个通用属性不能共存。而多个接口可以共同实现。

6.2 对象克隆

调用clone()执行复制而不是引用。但需要注意,clone方法对属性中的自定义类型只能进行shallow copy。

而且clone方法是protected的,意味着类只能克隆自己。

6.3 接口与回调

callback是一种常见的程序设计模式,一般我在js见的比较多,或者说C++中的函数指针,在某个事件发生后,直接调用指定的这个可变的函数。

在java中,传递的是一个实现了指定接口的对象。例子就不举了

6.4 内部类

即定义在另一个类中的类。内部类可以访问该类定义所在的作用域中的所有数据,并相对于同一个包隐藏起来。

当想要使用一个回调函数而又不想编写过多代码时,可以使用匿名内部类。

C++使用的是嵌套类。嵌套时类之间的关系而并不是对象之间的关系。对于一个嵌套类,可能并不会实现嵌套内的类。而内部类中里面的类会有一个隐式引用,指向实例化该内部对象的外围类对象,因此会很有意思。

static内部类则没有这种附加指针,与C++的嵌套类类似。

6.4.1 使用内部类访问对象状态

内部类可以隐式地访问创建它的外部对象,并使用外部对象域中的所有数据。这个引用在内部类中是不可见的(类似于外部嵌套的块作用域)

这个步骤是编译器自动在构造器中完成的(不太清楚,可能需要自行进行实验)

6.4.2 内部类的特殊语法规则

从正规来说,外围类引用的语法是比较复杂的。OuterClass.this表示外围类的引用,其中OuterClass为外围类的类名。

同时,构造器也可以使用更直观的方式outer Object.new InnerClass(construction parameters)

在外围类的作用域外,可以这样引用内部类:OuterClass.InnerClass

6.4.3 内部类是否有用、必要和安全

编译器使用了特殊的方法来访问,这原则上是破坏了封装。理论上,熟悉类文件结构的黑客可以创建虚拟机指令来调用指定方法的类文件。

6.4.4 局部内部类

即直接在类方法中定义类,这样它的作用域被限定在声明这个局部块中,完全与外部世界隔绝。

6.4.5 由外部方法访问final变量

局部类不仅能够访问外部类,还能够访问局部变量。不过,局部变量必须声明为final。

声明为final的原因是需要局部变量与局部类内建立的拷贝要保持一致。

如果仍然需要更新,则该局部变量可以声明为一个长度为1的数组。数组引用不变,但是其值可以改变,有点类似于C++的常数指针。

6.4.6 匿名内部类

例子

public void start(int interval,final boolean beep){
    ActionListener listener = new ActionListener(){
        public void actionPerformed(ActionEvent event){
            Date now = new Date();
            System.out.pritnln("At the tone, the time is "+now);
            if(beep) Toolkit.getDefaultToolkit().beep();
        }
    };
    Timer t = new Timer(interval,listener);
    t.start();
}

格式类似于new SuperType(construction parameters){inner class methods and data}

其中SuperType可以是接口或者要扩展的类。由于匿名类没有类名,自然也就没有构造器。取而代之,构造器参数传递给超类的构造器。尤其是内部类实现接口的时候,不能有任何构造参数。

6.4.7 静态内部类

如果使用内部类知识为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。可以将内部类声明为static,以便取消产生的引用。

当然,只有内部类可以声明为static。静态内部类不存在对生成它的外围类的引用,其他完全一样。在静态方法中构造的内部类必须为静态内部类。

6.5 代理

运用代理可以在运行时创建一个实现了一组给定接口的新类。该功能只在编译时无法确定需要实现哪个接口时才使用。

我不太感兴趣,直接跳过。

第11章 异常、断言、日志和调试

11.1 处理错误

异常分类:所有异常都是由Throwable继承而来。下一层是Error和Exception,Exception下分IOException和RuntimeException。

Error描述了Java运行时系统地内部错误和资源耗尽错误,应用程序不应该抛出这种类型的对象,基本无能为力。

11.1.2 声明已检查异常

如果遇到了无法处理的情况,java方法可以抛出一个异常。因此方法需要告诉编译器可能发生什么错误,在其首部声明可能抛出的异常。public FileInputStream(String name) throws FileNotFoundException,对构造器进行声明,说明可能会抛出FileNotFoundException异常。

多个已检查异常应该使用逗号隔开

不需要声明Java的内部错误(从Error继承的错误),因为任何代码都可能抛出,无法控制。同样,也不应该声明从RuntimeException继承的未检查异常,对于这些错误,更应该将时间花费在修正程序中的错误,而不是说明这些错误发生的可能性上。

11.1.3 如何抛出异常

//1
throw new EOFException();
//或者,2
EOFException e = new EOFException();
throw e;

这个EOFException还接受一个String类型的参数。

  1. 找到一个合适的异常类
  2. 创建这个类的一个对象
  3. 将对象抛出

11.1.4 创建异常类

可能会遇到标准异常类都没有能够充分描述清楚的问题,需要自定义异常类。需要定义一个派生于Exception的类,或者派生于Exception子类的类。

习惯上,该类要包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类的toString()方法会打印出这些详细信息)

11.2 捕获异常

使用

try{
    //code
}catch (ExceptionType e){
    //do something
}

来完成异常的捕获。如果方法中的任何代码抛出了catch子句中没有声明的异常类型,那么这个方法就会立刻退出。

11.2.1 捕获多个异常

一个try块可以捕获多个异常,每个异常使用一个单独的catch子句

11.2.2 再次抛出异常与异常链

try{
//some code access db
}catch(SQLException e){
    throw new ServeletException("database error:"+e.getMessage());
    //或者这样
    Throwable se = new ServeletException("database error");
    se.initCause(e);
    throw se;
    //捕获到异常时,可以使用Throwable e = se.getCause();获得原始异常
}

这方面的内容可以看一下这个问题,还挺有意思的。

11.2.3 finally子句

主要是为了解决资源回收问题,比如关闭说几句

不管是否有异常被捕获,finally子句中的代码都被执行。因为在大部分语言中都有这一部分,我就略过了。

11.2.5 分析堆栈跟踪元素

可以调用Throwable类的printStackTrace方法访问堆栈跟踪的文本描述信息。或者使用getStackTrace方法获得StackTraceElement对象的一个数组,从而自行分析。

11.4 使用断言

在python中经常用assert的选手应该很熟悉这个。即在一些非常关键、非常确信的地方使用该语句,以保证程序的正常运行。

Java中包括assert 条件;assert 条件:表达式;这两种。如果结果为false,则会抛出一个AssertionError异常。第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。

11.4.1 启用和禁用断言

感觉和python挺不一样的,更多是作为调试手段。可以在运行程序时使用-enableassertions-ea选项启用。在启用或禁用断言时不必重新编译程序。

也可以在某个包或某个类内使用断言:java -ea:MyClass -ea:com.mycompany.lib... MyApp

11.4.2 使用断言完成参数检查

断言的使用场景:

  • 断言的失败是致命的、不可恢复的错误。
  • 只用于开发和测试阶段。

11.5 记录日志

11.5.1 基本日志

日志系统管理着一个Logger.global的默认日志记录器Logger.getGlobal().info("File->Open menu item seelcted");,会自动包含时间、调用的类名和方法名。

但如果在相应的地方调用Logger.getGlobal().setLevel(Level.OFF);,将会取消所有的日志。

11.5.2 高级日志

自定义日志记录器,调用getLogger方法可以创建或检索记录器

private static final logger myLogger = Logger.getLogger("com.mycompany.myapp")

日志记录器级别:SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST,默认只记录前三个级别。

默认的日志记录将显示日志调用的类名和包名,但如果虚拟机对执行过程进行了优化,就得不到准确地调用信息,此时可以使用logp方法获得调用类和方法的确切位置。

11.5.3 修改日志管理器配置

配置文件优先于main方法调用。

不感兴趣,略过。

11.5.5 处理器

处理器可以处理日志记录器发来的记录。对于一个要被记录的日志记录,它的日志记录级别必须高于日志记录器和处理器的阈值。日志管理器配置文件设置的默认控制台处理器的日志记录级别为java.util.logging.ConsoleHandler.level=INFO

另外,还可以安装自己的处理器

Logger logger = Logger.getLogger("com.mycompany.myapp");
logger.setLevel(Level.FINE);
logger.setUserParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
logger.addHandler(handler);

日志到此略过,感觉这些都好老了。

11.6 调试技巧

…建议使用JUnit编写单元测试。

第12章 泛型程序设计

和C++比较类似,我估计一时半会用不上,先跳过。

一些值得注意的点

调用时可以省略泛型,编译器可以根据参数自动推断。

12.4 类型变量的限定

public static <T extends Comparable> T min(T[] a)

12.7 泛型类型的继承规则

Pair与Pair没什么关系。通常,Pair<S>Pair<T>没什么联系。

12.8 通配符类型

Pair<? extends Employee>表示任何泛型Pair类型,它的类型参数是Employee的子类。

12.8.1 通配符的超类型限定

很容易发现12.4的类型变量限定与其很类似。但它还有一个附加的能力,即可以指定一个超类型限定,如下所示? super Manager,这个通配符限制为Manager的所有超类型,可以为方法提供参数,但不能使用返回值。

例如void setFirst(? super Manager),编译器不知道setFirst方法的确切类型,但是可以用任意Managerr对象调用它,而不能用Employee对象调用。

12.8.2 无限定通配符

例如Pair<?>,返回值一般只能赋予Object

Pair<?>与Pair的不同在于,前者可以用任意Object对象调用原始的Pair类的指定方法

12.9 反射和泛型

12.8.3 通配符捕获

第13章 集合

说实话,这部分我建议红书Algorithm,单纯看操作很容易流于形式,还是需要结合具体的算法才比较好。

熟悉STL的话可以直接略过,没什么不同的。

13.1.2 Java类库中的集合接口和迭代器接口

Java类库中的集合类基本接口为Collection接口,有两个基本方法

public interface Collection<E>
{
    boolean add(E element);//用于向集合中增加元素
    Iterator<E> iterator();//返回一个实现了Iterator接口的对象,可以使用迭代器依次访问集合中的元素
}

迭代器:

public interface Iterator<E>
{
    E next();//通过反复调用next,可以逐个访问集合中的每个元素
    boolean hasNext();//如果还有多个可访问的元素,返回true
    void remove();//将会删除上次调用next方法时返回的元素(即当前所指的元素)。在调用remove前不调用next是不合法的。
}

迭代器的使用,比较优雅的可以使用For-each

for(String element:c){
    //do something with element
}

13.2 具体的集合

13.2.1 链表

动态数组ArrayList存在的问题时,从数组中间删除一个元素要付出巨大的代价。

为此实现了链表LinkedList,可以在任何位置高效地插入和删除。与泛型集合相比,链表是有序集合,其add方法可以将对象添加到链表的尾部或中间(由迭代器实现)。

链表可能出现问题,详见替代操作

List<String> list = new LinkedList<String>();
ListIterator<String> iter = list.listIterator();
String oldValue = iter.next();//拿到第一个值
iter.set(newValue); //给第一个值设置新值。这与前面remove的逻辑相同,在调用next后才能执行正确的逻辑。

13.2.3 散列集

散列表可以很快的计算出散列码,我不太清楚java的hash code是怎么算的,但一般来说都是唯一的。

HashSet类,散列表集合。该散列表使用的是桶实现,将散列表对桶的总数求余,得到的结果为保存这个元素的桶的索引。会碰上散列冲突问题,因此每个桶内部应该是链表。

13.2.4 树集

树集是有序集合,可以以任意形式插入但是顺序输出。TreeSet使用的是红黑树,效率会略高于HashSet,但是可以接受。

13.2.7 优先队列

说实话这个我用的比较多,因为红黑树比较难写。优先队列一般是堆,每次弹出优先级最高的任务。

13.2.8 映射表

map映射表,根据某些键的信息来查找与之对应的元素。HashMap是非常常用的工具。

与散列表不同,映射表中键是唯一的,同一个键中后赋的值会直接覆盖先赋的值。

13.3 集合框架

提供了一个从更高角度看类实现的方式,挺有意思的。写起来比较麻烦,建议看原书。

13.3.2 批操作

目前为止的大部分例子都使用迭代器来进行,同时也可以使用bulk operation批操作来避免频繁地使用迭代器。

比如

Set<String> result = new HashSet<>(a);
result.retainAll(b);//保留在a与b中都出现的元素看,构成交集

13.3.3 集合与数组之间的变换

数组变为集合可以使用Arrays.asList包装器

集合变数组可以使用toArray方法,但是不能直接调用(因为返回的是Object[]类型),而是应该String[] values=staff.toArray(new String[0]),将每个值在内部进行类型转换。

后续略过

第14章 多线程

java并发是非常复杂的,这里只能简单地学习一下

14.1 什么是线程

让我来回答的话就是进程内的子程序,是独立调度和分派的基本单元。多线程技术可以把容易阻塞的IO和人机交互功能与密集计算功能分开执行,从而提高程序的执行效率。

如何启动线程

  1. 将任务代码移到实现了Runable接口类的方法中。
public interface Runnable{
    void run();//这个接口非常简单,就只需要一个方法
}
class MyRunnable implements Runnable{
    public void run(){
        //task code
    }
}
  1. 创建一个类对象,并基于此创建Thread对象,启动进程。
Runnable r=  new MyRunnable();
Thread t = new Thread(r);
t.start()

PS:如果直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。

14.2 中断线程

自然终止:当线程的run方法执行方法体中的最后一条语句后,经由执行return语句返回,或者出现了方法中没有捕获的异常,线程将终止。

强制终止:调用interrupt方法可以用来请求终止线程。原理是调用该方法后,线程的中断状态将被置位。每个线程都会不时检查这个标识,以判断线程是否被中断。Thread.currentThread().isInterrupted()

但,如果线程被阻塞,就无法检测终端状态。此时会被Interrupted Exception异常中断。

没有任何一个语言方面的需求要求一个被中断的线程应该被终止,中断一个线程不过是为了引起它的注意,被中断的线程可以决定如何响应中断。线程一般将中断你作为一个终止的请求。

如果长期处于阻塞状态,应该检测InterruptedException异常

public void run(){
    try{
        while(more work to do){
            //do more work
            Trhead.sleep(delay);
        }
    }catch(InterruptedException e){
        //thread was interrupted during sleep
    }finally{
        //clean up, if required
    }
    //exiting the run method terminates the thread
}

14.3 线程状态

线程有六种状态:

  • New 新创建
  • Runnable 可运行
  • Blocked 被阻塞
  • Waiting 等待
  • Timed waiting 计时等待
  • Terminated 被终止

要确定一个线程的当前状态,可以调用getState方法

14.3.1 新创建线程

New:当调用new Thread(r)时,线程还没有开始运行

14.3.2 可运行线程

一旦调用thread的start方法,就是可运行状态。其是否运行,取决于操作系统给线程提供运行的时间。且一旦一个线程开始运行,它不必始终保持运行

14.3.3 被阻塞线程和等待线程

此时暂时不活动,直到线程调度器重新激活它。

  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当锁释放后,该线程将变为非阻塞状态
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。被阻塞状态与等待状态有很大的不同。
  • 有几个方法有超时参数,调用它们导致线程进入计时等待。这一状态将一直保持到超时期满或者接受到适当的通知。

14.3.4 被中止的线程

14.4 线程属性

包括线程优先级、守护线程、线程组以及处理未捕获异常的处理器。

线程优先级:默认情况下,一个线程继承它的父线程的优先级,可以使用setPriority方法设置一个MIN_PRIORITY1与MAX_PRIORITY10之间的任何值,默认是5.

14.4.2 守护线程

可以通过t.setDaemon(true)将线程转换为守护线程,为其他线程提供服务。如果只剩下守护线程,VM将退出。

守护线程不应该去访问固有资源,如文件、数据库因为它会在任何时候甚至任何一个操作的中间发生中断。

14.4.3 未捕获异常处理器

线程的run方法不能抛出任何被检测的异常。但是也不需要catch子句来处理可被传播的异常,在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。

该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法void uncaughtException(Thread t,Throwable e)。可以使用setUncaughtExceptionHandler方法为任何线程安装一个处理器,也可以用静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。

14.5 同步

这个就是操作系统的相关知识了,不赘述

14.5.3 锁对象

有两种机制防止代码块受到并发访问的干扰,一个是synchronized关键字,它自动提供了一个锁以及相关的条件,在需要显式锁的时候是很便利的。

第二个是ReentrantLock类,使用例子如下:

myLock.lock(); // a ReentrantLock object
try{
//critical section
}finally{
    myLock.unlock();
}

这一结构确保任何时候只有一个线程进入临界区。一旦一个线程封锁了对象,其他任何线程都无法通过lock语句,他们会被阻塞直到第一个线程释放锁对象。
PS:把解锁语句放在finally中至关重要,不然临界区的代码如果抛出异常,锁必须释放。

14.5.4 条件对象

线程进入临界区之后,要满足某一条件才能执行。该条件限制获得锁但是不能做有用工作的线程。

可以使用conditionName = newCondition()方法来设定新的条件对象。如果不满足条件,线程会调用conditionName.await()方法放弃锁。放弃锁与没获得锁有本质上的不同,它的阻塞状态直到条件满足后才能解除。

条件满足使用conditionName.signalAll()满足,激活所有因为这一条件等待的线程。

14.5.5 synchronized关键字

如果一个方法使用synchronized关键字声明,那么对象的锁将保护整个方法。即下面两个是相等的

public synchronized void method(){
    //method body
}
//等价于
public void method(){
    this.intrinsicLock.lock();
    try{
        //method body
    }finally{
        this.intrinsicLock.unlock();
    }
}

14.5.6 同步阻塞

每一个Java对象有一个锁,线程可以通过调用同步方式获得锁。另外一个获得锁的机制就是进入一个同步阻塞

synchronized(obj){
    //critical section
}

14.5.7 监视器概念

监视器monitor,可以在不需要程序员考虑加锁的情况下,保证多线程的安全性。

监视器的特性:

  • 是只包含私有域的类
  • 每个监视器类的对象有一个相关的锁
  • 使用该锁对所有方法进行加锁。比如调用obj.method(),则obj对象的锁在方法调用开始时自动获得,并且当方法返回时自动释放。
  • 该锁可以有任意多个相关条件

但是Java类和监视器差距很大

14.5.8 Volatile域

如果仅仅为了读写一个或两个实例域而使用同步,开销过大。

volatile域为实例域的同步访问提供了一种免锁的机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域可能被另一个线程并发更新。

private boolean done;
public synchronized boolean isDone(){return true;}
public synchronized void setDone(){done = true;}
//上述使用内部锁,但是并不一定是个好主意
private volatile boolean done;
public boolean isDone(){return true;}
public void setDone(){done =  true;}
//但是不保证原子性

14.5.14 读/写锁

java.util.concurrent.locks包定义了两个锁类,即ReentrantLock类和ReentrantReadWriteLock类。后者解决了操作系统中的读/写者问题,允许对读者线程共享访问控制,写者进行互斥访问控制。

  1. 创建对象private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  2. 声明读写锁private Lock readLock = rwl.readLock();private Lock writeLock = rwl.writeLock();
  3. 对所有获取方法加读锁
public double getTotalBalance(){
    readLock.lock();
    try{
        //some method
    }finally{
        readLock.unlock();
    }
}
  1. 对所有修改方法加写锁
public void transfer(...){
    writeLock.lock();
    try{
        //some method
    }finally{
        writeLock.unlock();
    }
}

14.6 阻塞队列

应该是为了解决生产者-消费者问题。实际编程应该尽量原理基本结构,并使用高层结构。

java.util.concurrent包提供了阻塞队列的几个变种。

14.7 线程安全的集合

如果多线程要并发地修改一个数据结构,那会很容易破坏它。

14.7.1 高效地映射表、集合和队列

java.util.concurrent包提供了映射表、有序集和队列的高效实现:

  • ConcurrentHashMap
  • ConcurrentSkipListMap
  • ConcurrentSkipListSet
  • ConcurrentLinkedQueue

与大多数集合不同,size方法不必在常量时间内操作,而是需要遍历。

14.8 Callable与Future

Runnable封装一个异步运行的任务。Callable与其类似,但是有返回值。

public interface Callable<V>{
    V call() throws Exception;
}

Future保存异步计算的结果(有点类似最近看到最新JS的那个future)。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future的所有者在结果计算好之后就可以获得它。

public interface Future<V>{
    V get() throws ...;
    V get(long timeout, TimeUnit unit) throws;
    void cancel(boolean mayInterrupt);
    boolean isCancelled();
    boolean isDone();
}

第一个get方法的调用被阻塞,直到计算完成;如果在计算完成之前,第二个方法的调用抄书,抛出一个TimeoutException异常。如果运行该计算的线程被中断,两个方法都将抛出InterruptedException。如果计算已经完成,那么get方法将立即返回。

可以使用cancel方法取消计算。

FutureTask包装器,可以将Callable转换成Future和Runnable,它同时实现两者的接口:

Callable<Integer> myComputation = ...;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
Thread t = new Thread(task);//task is a Runnable
t.start();
...
Integer result = task.get();//task is a Future

14.9 执行器

构建线程有一定的代价。如果程序中创建了大量生命期很短的线程,应该使用线程池(thread pool)。线程池中包含许多准备运行的空线程,将Runnable对象交给线程池,就会有一个线程调用run方法。

当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

另一个使用线程池的理由:减少并发线程的数目。

执行器Executor类,使用许多静态工厂方法来构建线程池:

  • newCachedThreadPool。构建线程池,如果有空闲线程可用则立刻执行;否则,创建一个新线程。
  • newFixedThreadPool
  • newSingleThreadExecutor
  • newScheduledThreadPool
  • newSingleThreadScheduledExecutor

14.10 同步器

java.util.concurrent包提供能帮助人们管理相互合作的线程集的类:

  • CyclicBarrier:允许线程集等待直到其中预定数目的线程到达一个公共障栅barrier,然后可以选择执行一个处理障栅的动作。
  • CountDownLatch:允许线程集等待直到计数器为0
  • Exchanger:允许两个线程在要交换的对象准备好时交换对象。
  • Semaphore:允许线程集等待直到被允许继续执行为止
  • SynchronousQueue:允许一个线程把对象交给另一个线程
 类似资料: