Java核心技术·卷Ⅰ(第10版)学习笔记

第四章:对象与类

  • 面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
  • 要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象应用方法。(96)
  • 构造器是一种特殊的方法,用来构造初始化对象。(构造方法)(96)
        Date birthday = new Date();
    在这里插入图片描述
  • 一个类通过使用new运算符可以创建多个不同的对象。这些对象将分配不同的内存空间,因此改变其中一个对象的状态不会影响其他对象的状态。
  • 一定要认识到:一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。(地址)(97)
  • 在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。(97)
  • 打印当前日历:
public static void main(String[] args) {
	LocalDate date=LocalDate.now();   //构造一个表示当前日期的对象。
	int month=date.getMonthValue();   //获取当前月
	int today=date.getDayOfMonth();   //获取当前月的第几天
	date=date.minusDays(today-1);   //生成当月的第一天
	int value=date.getDayOfWeek().getValue();   //返回 1~7,1=Monday,7=Sunday
	System.out.println("Mon Tue Wed Thu Fri Sat Sun");
	for(int i=1;i<value;i++)   //缩进
		System.out.print("    ");
	while(date.getMonthValue()==month) {   //若还是当前月则继续循环
		System.out.printf("%3d",date.getDayOfMonth());   //获取当前月的第几天
		if(date.getDayOfMonth()==today)   //当天
			System.out.print("*");
		else
			System.out.print(" ");
		date=date.plusDays(1);   //加一天,返回一个新对象,不可变性
		if(date.getDayOfWeek().getValue()==1)
			System.out.println();
	}
	if(date.getDayOfWeek().getValue()!=1)
		System.out.println();
}
  • 在一个源文件中, 只能有一个公有类,但可以有任意数目的非公有类。
  • 封装:一旦在构造器中(构造方法)设置完毕,只能通过访问器更改器对实例域修改,确保实例域不会受到外界的破坏。
  • 不要编写返回引用可变对象的访问器方法(110)
    在这里插入图片描述
        凭经验可知,如果需要返回一个可变数据域的拷贝,就应该使用 clone。(Date) hireDay.clone();这样无论如何改变date也不会影响harry中的hireDay了。    LocalDate具有不可变性,类似String,如果对其进行修改,则在一个新的对象上进行修改。
  • 一个方法可以访问所属类的所有对象的私有数据。(111)
class Employee{
	private String name;
	public boolean equals(Employee other) {
		return name.equals(other.name); <------这里访问了其它对象的私有属性
	}
}
Employee harry = new Employee();
Employee boss = new Employee();
if(harry.equals(boss))

    其原因是 boss 是 Employee 类对象, 而 Employee 类的方法可以访问 Employee 类的任何一个对象的私有域。
    关键字 private 确保只有 Employee 类自身的方法能够访问这些实例域,而其他类的方法不能够读写这些域。private的访问控制是针对来检查的,而非对象

  • 每一个雇员对象都有一个自己的 id 域,但这个类的所有实例将共享一个 nextid 域。换句话说,如果有1000个 Employee 类的对象,则有1000个实例域 id。但是,只有一个静态域 nextld。即使没有一个雇员对象,静态域 nextid 也存在。它属于类,而不属于任何独立的对象。
class Employee{
	private static int nextId = 1;
	private int id;
}
  • 静态方法是一种不能向对象实施操作的方法,可以认为静态方法是没有this参数的方法。
  • 2种情况下使用静态方法:一、方法不需要访问对象状态,其所需参数都是通过显式参数提供。二、一个方法只需要访问类的静态域,Employee.getNextid()
  • Java对对象采用的不是引用调用,对象引用是按值传递的。(拷贝的值)(局部变量)(因为一个对象变量并没有实际包含一个对象,而仅仅引用一个对象)(120)
///It doesn't work
public static void swap(Employee x, Employee y){
	Employee temp = x;
	x = y;
	y = temp;
}
swap(alice, bob);

    在方法结束时参数变量 x 和 y 被丢弃了。原来的变量 alice 和 bob 仍然引用这个方法调用之前所引用的对象。
在这里插入图片描述

  • 一个方法不能修改一个基本数据类型的参数(数值型或布尔型);一个方法可以改变一个对象参数的状态;一个方法不能让对象参数引用一个新的对象。(120)
  • 方法的签名:要完整地描述一个方法,需要指出方法名以及参数类型。返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。(123)
  • 仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器。(124)
  • this 是隐式参数,即所构造的对象。如果构造器的第一个语句形如 this(…), 这个构造器将调用同一个类的另一个构造器。(126)
public Employee(double s){
	//call Employee(String,double)
	this("Employee #" + nextId,s);
	nextId++
}
  • 初始化块,每次创建新对象都会执行一次(127)
// object initialization block
{
	id = nextId;
	nextId++;
}
  • 静态的初始化块,第一次创建对象时执行一次,其它时候都不执行(128)
//static initialization block
static
{
	Random generator = new Random0;
	nextld = generator.nextlnt(lOOOO);
}
  • 类设计技巧:(144)
  1. 一定要保证数据私有,不要破坏封装性。
  2. 一定要对数据初始化。Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。
  3. 不要在类中使用过多的基本类型。就是说,用其他的类代替多个相关的基本类型的使用。
  4. 不是所有的域都需要独立的域访问器和域更改器
  5. 将职责过多的类进行分解
  6. 类名和方法名要能够体现它们的职责
  7. 优先使用不可变的类。LocalDate 类以及 java.time 包中的其他类是不可变的—没有方法能修改对象的状态
        类似 plusDays 的方法并不是更改对象,而是返回状态已修改的新对象。
        更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。

第五章:继承

在这里插入图片描述

  • 继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。(147)
  • 关键字 extends 表示继承,表明正在构造的新类派生于一个已存在的类。(148)
        public class Manager extends Employee
  • 已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类 (derived class)或孩子类(child class)(148)
  • 尽管子类有与超类相同的实例域,但是在子类本身及其方法不能直接访问超类的拥有的实例域,只能通过超类的public方法才能访问超类的私有域。(this.salary不能访问,super.salary不能访问,只能super.getSalary())(149)
  • super关键字有两个用途:一是调用超类的方法,二是调用超类的构造器。
  • 一个对象变量(Employee e)可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。(151)
import java.time.LocalDate;

public class Employee {
	private String name;
	private double salary;
	private LocalDate hireDay;
	
	public Employee() {}
	
	public Employee(String name,double salary,int year,int month,int day) {
		this.name=name;
		this.salary=salary;
		this.hireDay=LocalDate.of(year, month, day);
	}
	
	public String getName() { return name; }
	
	public double getSalary() { return salary; }
	
	public LocalDate getHireDay() { return hireDay; }
	
	public void raiseSalary(double byPercent) { this.salary+=salary*byPercent/100; }
}

public class Manager extends Employee{

	private double bonus;
	
	public Manager() { super(); }
	
	public Manager(String name,double salary,int year,int month,int day) {
		super(name, salary, year, month, day);
		bonus=0;
	}
	
	@Override
	public double getSalary() { return super.getSalary()+ bonus; }
	
	public void setBonus(double bonus) { this.bonus = bonus; }
}

public class Test {
	public static void main(String[] args) {
		Manager boss=new Manager("Carl",80000,1987,12,15);
		boss.setBonus(5000);
		Employee[] staff=new Employee[3];
		staff[0]=boss;  //多态
		staff[1]=new Employee("Harry", 50000, 1989, 10, 1);
		staff[2]=new Employee("Tommy", 40000, 1990, 3, 15);
		for(Employee e:staff)
			System.out.println("name= "+e.getName()+", salery= "+e.getSalary()+", hireDay= "+e.getHireDay());
	}
}
  • 由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。一个祖先类可以拥有多个子孙继承链。
  • 子类的每个对象都是超类的对象。例如,每个经理都是雇员,而不是每一名雇员都是经理。(154)
  • 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。(156)
        Employee类:public Employee getBuddy(){ . . . }
        Manager类:public Manager getBuddy(){ . . . } // OK to change return type
  • 方法调用过程:(155)
  1. 编译器査看对象的声明类型和方法名。有可能存在多个名字相同, 但参数类型不一样的方法。编译器将会列举所有 C 类中名为 f 的方法和其超类中访问属性为public且名为 f 的方法。
  2. 编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供的参数类型完全匹配,就选择这个方法(重载解析)。
  3. 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。如果不是这些方法,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。
  4. 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。
  • 在覆盖一个方法的时候,子类方法的权限不能低于超类方法的权限。特别是,如果超类方法是public, 子类方法一定要声明为public。经常会发生这类错误:在声明子类方法的时候,遗漏了public修饰符。此时,编译器将会把它解释为试图提供更严格的访问权限。(157)
  • 如果在定义的时候使用了 final 修饰符就表明这个类不允许扩展(不能被继承)(157)
  • 如果在定义方法的时候使用了 final 修饰符,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法,但不包括实例域 )(158)
  • String类是final类,这意味着不允许任何人定义String的子类。所以,如果有一个String的引用,它引用的一定是一个String对象,而不可能是其他类的对象。(158)
  • 如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联 (inlining)。例如,内联调用e.getName()将被替换为访问e.name域。如果方法很简短、被频繁调用且没有真正地被覆盖,那么即时编译器就会将这个方法进行内联处理。如果虚拟机加载了另外一个子类,而在这个子类中包含了对内联方法的覆盖,优化器将取消对覆盖方法的内联。(158)
  • 在进行类型转换之前,先使用 instanceof 操作符查看是否能够成功转换,如果这个类型转换不成功,编译器就不会进行这个转换。只能在继承层次内进行类型转换。(159)
// staff[1]能否转化为Manager类型
if (staff[1] instanceof Manager){
	boss = (Manager) staff[1]:    //尽量少用instanceof
}
  • 每个人都有一些诸如姓名这样的属性。学生与雇员都有姓名属性,因此可以将getName方法放置在位于继承关系较高层次的通用超类中。(161)
  • 包含一个或多个抽象方法的类本身必须被声明为抽象的。(161)
  • 扩展抽象类有两种选择:一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,子类就不是抽象的了。(161)
  • 类即使不含抽象方法,也可以将类声明为抽象类。(162)
  • 抽象类不能被实例化。也就是说,如果将一个类声明为 abstract, 就不能创建这个类的对象。可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。(162)
  • 如果希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。需要将这些方法或域声明为protected。(165)
  • “如果将超类Employee中的hireDay声明为proteced,而不是私有的,Manager中的方法就可以直接地访问它。不过Manager类中的方法只能够访问Manager对象中的hireDay域,而不能访问其他Employee对象中的这个域。”(165)
    ---->就是说你只能访问你继承的这个类的hireday域,比如:你的Manager类是继承Employee的,那么你就能在这个类里访问hireday域(可以访问同一个类的私有域)。但是其他同样继承Employee类中的hireday域你是无法访问的。(即只能访问同一条继承链的域)如果你定义的是private,子类根本无法直接访问。
  • Java用于控制可见性的4个访问修饰符(针对类):(165)
    1. 仅对本类可见----private
    2. 对本包可见----默认,不需要修饰符
    3. 对本包和所有子类可见----protected
    4. 对所有类可见----public
  • Object类是Java中所有类的超类,在Java中每个类都是由它扩展而来的。(166)
  • 在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。(166)
Employee[] staff = new Employee[10];
Object obj = staff; // OK
Object obj = new int[10]; // OK
  • Objects.equals(a,b):如果两个参数都为 null,Objects.equals(a,b)调用将返回 true;如果其中一个参数为 null,则返回false ;否则,如果两个参数都不为null,则调用a.equals(b)。(167)
  • 在子类中定义equals方法时,首先调用超类的equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。(167)
  • Java语言规范要求equals方法具有下面的特性:(168)
    1. 自反性:对于任何非空引用x,x.equals(x)应该返回true。
    2. 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
    3. 传递性:对于任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,x.equals(z)也应该返回true。
    4. 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
    5. 对于任意非空引用x,x.equals(null)应该返回false
  • 如果隐式和显式的参数不属于同一个类,equals方法将如何处理呢?(168)
        尽量少用instanceof,因为它违反了对称性。equals()中使用了instanceof
        Employee.equals(Manager) //correct—>if(Employee instanceof Manager)
        Manager.equals(Employee) //false—>if(Manager instanceof Employee)
        如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
        如果由超类决定相等的概念,那么就可以使用intanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。
        在雇员和经理的例子中,只要对应的域相等,就认为两个对象相等。如果两个Manager 对象所对应的姓名、薪水和雇佣日期均相等,而奖金不相等,就认为它们是不相同的,因此,可以使用getClass检测。
        但是,假设使用雇员的ID作为相等的检测标准,并且这个相等的概念适用于所有的子类,就可以使用instanceof进行检测,并应该将Employee.equals声明为final。

编写一个完美的equals方法的建议:(169)

  1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做 other 的变量。
  2. 检测this与otherObject是否引用同一个对象:
    if (this == otherObject) return true;
    这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。
  3. 检测otherObject是否为null,如果为 null,返回false。这项检测是很必要的。
    if (otherObject == null) return false;
  4. 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:
    if (getClass() != otherObject.getClass()) return false;
    如果所有的子类都拥有统一的语义,就使用instanceof检测:
    if (!(otherObject instanceof ClassName)) return false;
  5. 将otherObject转换为相应的类类型变量:
    ClassName other = (ClassName) otherObject
  6. 现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true; 否则返回false。
    return fieldl == other.field && Objects.equa1s(fie1d2, other.field2)
    如果在子类中重新定义equals, 就要在其中包含调用super.equals(other)。
  • 最好使用null安全的方法Objects.hashCode。如果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。(171)
  • equals与hashCode的定义必须一致:如果 x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。例如,如果用定义的 Employee.equals比较雇员的ID,那么hashCode方法就需要散列ID,而不是雇员的姓名或存储地址。(172)
  • 随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述。System.out.println(x);就会直接地调用 x.toString()(173)

import java.time.LocalDate;
import java.util.Objects;

public class Employee {
	private String name;
	private double salary;
	private LocalDate hireDay;
	
	public Employee() {}
	
	public Employee(String name,double salary,int year,int month,int day) {
		this.name=name;
		this.salary=salary;
		this.hireDay=LocalDate.of(year, month, day);
	}
	
	public String getName() { return name; }
	
	public double getSalary() { return salary; }
	
	public LocalDate getHireDay() { return hireDay; }
	
	public void raiseSalary(double byPercent) { this.salary+=salary*byPercent/100; }
	
	public boolean equals(Object otherObject) {
		//a quick test to see if the objects are identical
		if(otherObject==this)
			return true;
		//must return false if the explicit parameter is null
		if(otherObject==null)
			return false;
		//if the classes don't match,they can't be equal
		if(getClass()!=otherObject.getClass())
			return false;
		//now we know otherObject is a non-null Employee
		Employee other=(Employee)otherObject;
		//test whether the fields have identical values
		return Objects.equals(name, other.name)&&salary==other.salary&&Objects.equals(hireDay, other.hireDay);
	}
	
	public int hashCode() { return Objects.hash(name,salary,hireDay); }
	
	public String toString() {
		return getClass().getName()+"[name= "+name+",salary= "+salary+",hireDay= "+hireDay+"]";
	}
}

public class Manager extends Employee{

	private double bonus;
	
	public Manager() { super(); }
	
	public Manager(String name,double salary,int year,int month,int day) {
		super(name, salary, year, month, day);
		bonus=0;
	}
	
	@Override
	public double getSalary() { return super.getSalary()+bonus; }
	
	public void setBonus(double bonus) { this.bonus = bonus; }

	public boolean equals(Object otherObject) {
		if(!super.equals(otherObject))
			return false;
		Manager other=(Manager)otherObject;
		return other.bonus==this.bonus;
	}
	
	public int hashCode() { return super.hashCode()+17*Double.hashCode(bonus); }
	
	public String toString() { return super.toString()+"[bonus= "+bonus+" ]"; }
}

public class Test {
	public static void main(String[] args) {
		Employee alice1=new Employee("Alice",75000,1987,12,15);
		Employee alice2=alice1;
		Employee alice3=new Employee("Alice",75000,1987,12,15);
		Employee bob=new Employee("Bob",50000,1989,10,1);
		System.out.println("alice1==alice2: "+(alice1==alice2));
		System.out.println("alice2==alice3: "+(alice2==alice3));
		System.out.println("alice1.equals(alice3): "+alice1.equals(alice3));
		System.out.println("alice1.equals(bob): "+alice1.equals(bob));
		System.out.println("bob.toString: "+bob.toString());
		System.out.println("alicel.hashCode(): " + alice1.hashCode());
		System.out.println("alice3.hashCode(): " + alice3.hashCode());
		System.out.println("bob.hashCode(): " + bob.hashCode());
		Manager carl=new Manager("Carl",80000,1987,12,15);
		Manager boss=new Manager("Carl",80000,1987,12,15);
		boss.setBonus(5000);
		System.out.println("boss.toString():" + boss);
		System.out.println("carl.toString(): "+carl.toString());
		System.out.println("carl.equals(boss): " + carl.equals(boss));
		System.out.println("carl.hashCode() : " + carl.hashCode());
	}
}

运行结果:
alice1==alice2: true
alice2==alice3: false
alice1.equals(alice3): true
alice1.equals(bob): false
bob.toString: study.Employee[name= Bob,salary= 50000.0,hireDay= 1989-10-01]
alicel.hashCode(): 172801614
alice3.hashCode(): 172801614
bob.hashCode(): -533108427
boss.toString():study.Manager[name= Carl,salary= 80000.0,hireDay= 1987-12-15][bonus= 5000.0 ]
carl.toString(): study.Manager[name= Carl,salary= 80000.0,hireDay= 1987-12-15][bonus= 0.0 ]
carl.equals(boss): false
carl.hashCode() : 1436354726

  • ArrayList是一个采用类型参数(type parameter)的泛型类(generic class)。允许运行时动态更改数组(174)
    ArrayList<Employee> staff = new ArrayList<>();
    这被称为“ 菱形” 语法,因为空尖括号<>就像是一个菱形。
    如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用 ensureCapacity方法:staff.ensureCapacity(100);
    这个方法调用将分配一个包含100个对象的内部数组。然后调用100次add,而不用重新分配空间。另外,还可以把初始容量传递给ArrayList构造器: ArrayList<Employee> staff = new ArrayList<>(100);
  • 所有的基本类型都有一个与之对应的类。 例如,Integer类对应基本类型int。通常, 这些类称为包装类(wrapper)。这些对象包装类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean(前6个类派生于公共的超类Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。(184)
  • 假设想定义一个整型数组列表。而<>中的类型参数不允许是基本类型,也就是说,不允许写成 ArrayList。这里就用到了Integer对象包装器类。我们可以声明一个Integer对象的数组列表。ArrayList<Integer> staff = new ArrayList<>();
  • list.add(3);自动地变换成list.add(Integer.value0f(3));这种变换被称为自动装箱(autoboxing),由基本类型自动转化为对象。(184)
  • int n = list.get(i);自动地变换成int n = list.get(i).intValue();将会自动拆箱。(185)
  • Integer n = 3; n++; 编译器将自动地插人一条对象拆箱的指令,然后进行自增计算,最后再将结果装箱。(185)
  • 如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double:(185)
  • 参数数量可变的方法:省略号 表明这个方法可以接收任意数量的对象,实际上,它是一个数组。(188)
public static double max (double... values){
	double largest = Double.NEGATIVEJNFINITY;
	for (double v : values)
		if (v > largest) largest = v;
	return largest;
}

        调用double m = max(3.1,40.4,-5);,编译器将new double[]{3.1,40.4,-5} 传递给 max 方法。

  • 枚举类型是一个类,所有的枚举类型都是Enum类的子类。在比较两个枚举类型的值时,永远不需要调用equals,直接使用“==”就可以了。(188)
public enun Size {SMALL,MEDIUM,LARGE,EXTRALARGE};
  • 所谓枚举类就是有包含有固定数量实例(并且实例的值也固定)的特殊类,如果其含有public构造器,那么在类的外部就可以通过这个构造器来新建实例,显然这时实例的数量和值就不固定了,这与定义枚举类的初衷相矛盾,为了避免这种形象,就对枚举类的构造器默认使用private修饰。如果为枚举类的构造器显式指定其它访问控制符,则会编译出错。枚举被设计成是单例模式,即枚举类型会由JVM在加载的时候,实例化枚举对象,你在枚举类中定义了多少个就会实例化多少个,JVM为了保证每一个枚举类元素的唯一实例,是不会允许外部进行new的,所以会把构造函数设计成private,防止用户生成实例,破坏唯一性。
public enum Gender{
    //枚举类的所有实例必须在其首行显式列出,否则它不能产生实例。
    //通过括号赋值,而且必须带有一个参构造器,否则编译出错
    //赋值必须都赋值或都不赋值,不能一部分赋值一部分不赋值;如果不赋值则不能写构造器,赋值编译也出错
	MAN("MAN"), WOMEN("WOMEN");

    private final String value;

    //构造器默认也只能是private, 从而保证构造函数只能在内部使用
    Gender(String value) {
        this.value = value;
    }
        
    public String getValue() {
        return value;
    }
}
enum Size{
	SMALL("S"),MEDIUM("M"),LARGE("L"), EXTRALARGE("XL");  //构造实例

	private String abbreviation;
	
	private Size(String abbreviation) {   //每个值调用一次
		this.abbreviation=abbreviation;
		System.out.println(abbreviation);
	}
	
	public String getAbbreviation() { return abbreviation; }
}

public static void main(String[] args) {
	Scanner in = new Scanner(System.in);
	System.out.print("Enter a size: (SMALL, MEDIUM, LARGE, EXTRALARGE) ");
	String input = in.next().toUpperCase();
	Size size = Enum.valueOf(Size.class, input);
	System.out.println("size=" + size);
	System.out.println("abbreviation=" + size.getAbbreviation());
	if (size == Size.EXTRALARGE)
		System.out.println("EXTRA_LARGE");
}

运行结果:
Enter a size: (SMALL, MEDIUM, LARGE, EXTRALARGE) EXTRALARGE
S M L XL
size=EXTRALARGE
abbreviation=XL
EXTRA_LARCE

  • 在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。保存这些信息的类被称为 Class(190)
  • 获取Class类对象的方法:e.getClass()、Class.forName(className)、T.class(190)
  • 异常有两种类型:未检查异常已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常。编译器不会査看是否为这些错误提供了处理器。(192)
  • 继承的设计技巧:(208)
    1. 将公共操作和域放在超类。如:姓名、性别、年龄等。
    2. 不要使用受保护(protected)的域。protected机制并不能够带来更好的保护,其原因主要有两点:一、子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问protected的实例域,从而破坏了封装性。二、在Java程序设计语言中,在同一个包中的所有类都可以访问proteced域,而不管它是否为这个类的子类。不过,protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
    3. 使用继承实现“is-a”关系。比如:Manager继承Employee,Manager is a Employee。而钟点工不能继承雇员类,因为它的工资是按小时计的,如果继承雇员类会有薪水和计时工资两个域,所以钟点工is not a Employee。
    4. 除非所有继承的方法都有意义,否则不要使用继承
    5. 在覆盖方法时,不要改变预期的行为
    6. 使用多态,而非类型信息。无论什么时候,对于下面这种形式的代码
      if(x is of type1)
      action1(x);
      else if(x is of type2)
      action2(x);
      都应该考虑使用多态性。action1与action2表示的是相同的概念吗?如果是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类的超类或接口中,然后, 就可以调用x.action(); 以便使用多态性提供的动态分派机制执行相应的动作。使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
    7. 不要过多地使用反射。反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

第六章:接口

  • 在 Java 程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。(211)
  • 接口中的所有方法自动地属于public。因此,在接口中声明方法时,不必提供关键字 public。实现接口要实现接口中的所有方法。(212)
  • 接口可以定义常量,接口绝不能含有实例域。提供实例域和方法实现的任务应该由实现接口的那个类来完成。(212)
  • 接口中的域将被自动设为 public static final。(217)
  • 尽管每个类只能够拥有一个超类,但却可以实现多个接口。(217)
  • 在 Java SE 8 中,允许在接口中增加静态方法。(218)
  • 假设希望每隔10s打印一条信息“At the tone,the time is . . .”,然后响一声。
    在这里插入图片描述
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
import javax.swing.JOptionPane;
import javax.swing.Timer;

class TimePrinter implements ActionListener{   //实现接口
	public void actionPerformed(ActionEvent event) {
		System.out.println("At the tone,the time is "+new Date());
		Toolkit.getDefaultToolkit().beep();
	}
}

public class Test {
	public static void main(String[] args) {
		ActionListener listener=new TimePrinter();
		Timer timer=new Timer(5000, listener);
		timer.start();
		JOptionPane.showMessageDialog(null, "Show time and beep");   //没有这个程序执行完就不执行线程了
		System.exit(0);
	}
}

在这里插入图片描述

  • 为一个包含对象引用的变量建立副本时,原变量和副本都是同一个对象的引用,任何一个变量改变都会影响另一个变量。(225)
Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(lO); // oops-also changed original

        如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用 clone 方法。

Employee copy = original,clone();
copy.raiseSalary(lO); // OK original unchanged

        clone方法是Object的一个protected方法,这说明你的代码不能直接调用这个方法。只有Employee类可以克隆Employee对象。这个限制的原因是Object类对于这个对象一无所知,所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样原对象和克隆的对象仍然会共享一些信息。
在这里插入图片描述

  • 默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String,或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。(226)

  • 通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝, 同时克隆所有子对象。
    在这里插入图片描述

  • Cloneable接口是Java提供的一组标记接口(tagging interface)之一。(227)

  • 即使clone的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable接口,将 clone重新定义为public,再调用super.clone()。clone方法的返回类型总是Object。如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会拋出一个CloneNotSupportedException()。

class Employee implements Cloneable{
	// raise visibility level to public, change return type
	public Employee clone() throws CloneNotSupportedException{
		// call Object,clone()
		Employee cloned = (Employee) super.clone() ;
		// clone mutable fields
		cloned.hireDay = (Date) hireDay. clone();  //深拷贝
		return cloned;
	}
	...
}
  • 所有数组类型都有一个public的clone方法,而不是protected:可以用这个方法建立一个新数组,包含原数组所有元素的副本。(229)
int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
int[] cloned = luckyNumbers.clone();
cloned[5] = 12; // doesn't change luckyNumbers[5]
  • lambda 表达式是一个可传递的代码块,可以在以后执行一次或多次。(231)
  • 使用内部类的主要原因:(242)
    1.内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
    2.内部类可以对同一个包中的其他类隐藏起来。
    3.当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
  • 内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域.(244)
  • 内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。这个引用在内部类的定义中是不可见的。(244)
            内部类虽然和外部类写在同一个文件中,但是编译完成后, 还是生成各自的class文件。
    在这里插入图片描述
  1. 编译器自动为内部类添加一个成员变量,这个成员变量的类型和外部类的类型相同,这个成员变量就是指向外部类对象(this)的引用;
ActionListener listener = new TimePrinter(this); // parameter automatically added
  1. 编译器自动为内部类的构造方法添加一个参数,参数的类型是外部类的类型,在构造方法内部使用这个参数为内部类中添加的成员变量赋值;
public TimePrinter(TalkingClock clock) // automatically generated code{
	outer = clock;
}
  1. 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。
public void actionPerformed(ActionEvent event){
	System.out.println('At the tone, the time is " + new Date());
	if (outer.beep) Toolkit.getDefaultToolkit().beep();
}
  • 只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。(245)
class TalkingClock{
	private int interval;
	private boolean beep;
	
	public TalkingClock(int interval,boolean beep) {
		this.interval=interval;
		this.beep=beep;
	}
	
	public void start() {
		ActionListener listener=new TimePrinter();
		Timer timer=new Timer(interval, listener);
		timer.start();
	}
	
	public class TimePrinter implements ActionListener {
		public void actionPerformed(ActionEvent event) {
			System.out.println("At the tone,the time is "+new Date());
			if(beep) Toolkit.getDefaultToolkit().beep();
		}
	}
}

public class InnerClassTest {
	public static void main(String[] args) {
		TalkingClock clock = new TalkingClock(1000, true);
		clock.start();
		JOptionPane.showMessageDialog(null, "Do you want to quit?");
		System.exit(0);
	}
}
  • OuterClass.this表示外围类引用。(247)
    outerObject.new InnerClass {construction parameters)表示内部对象的构造器
    在外围类的作用域之外引用内部类:OuterClass.InnerClass
    TalkingClock jabberer = new Ta1kingClock(1000, true);
    TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

  • 内部类中声明的所有静态域都必须是final。原因很简单。我们希望一个静态域只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例。如果这个域不是final,它可能就不是唯一的。(247)

  • 可以在一个方法中定义局部内部类。局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。(250)

public void start(){
	class TimePrinter inplements ActionListener{
		public void actionPerformed(ActionEvent event){
			System.out.println("At the tone, the tine is " + new Date())if (beep) Toolkit.getDefaultToolkit().beep():
		}
	}
	ActionListener listener = new TimePrinter();
	Timer t = new Timer(interva1, listener);
	t.start();
}

        局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。局部类还有一个优点,它们不仅能够访问包含它们的外部类,还可以访问局部变量。

public void start(int interval, boolean beep){ <---方法结束后,局部变量就没有了,但在构造内部类时,会拷贝一份
	class TimePrinter inplements ActionListener{
		public void actionPerformed(ActionEvent event){
			System.out.println("At the tone, the tine is " + new Date())if (beep) Toolkit.getDefaultToolkit().beep():
		}
	}
	ActionListener listener = new TimePrinter();
	Timer t = new Timer(interva1, listener);
	t.start();
}
  • 匿名内部类能实现事件监听器和其他回调。匿名内部类会导致内存泄漏,这是所有非静态内部类都有的一个致命的问题–他们会在内部维护一个外部容器类的引用。非静态内部类为什么持有外部类的引用?因为非静态内部类依赖着外部类,没有外部类就不能创建内部类。
class TalkingClock{
	public void start(int interval,boolean beep) {
		ActionListener listener=new ActionListener(){
			public void actionPerformed(ActionEvent event) {
			System.out.println("At the tone,the time is "+new Date());
			if(beep) Toolkit.getDefaultToolkit().beep();
		}};
		Timer timer=new Timer(interval, listener);
		timer.start();
	}
}

public class InnerClassTest {
	public static void main(String[] args) {
		TalkingClock clock = new TalkingClock();
		clock.start(1000, true);
		JOptionPane.showMessageDialog(null, "Do you want to quit?");
		System.exit(0);
	}
}
  • 静态内部类将内部类声明为 static,静态内部类可以有静态域和方法。不需要引用任何其他的对象。在内部类不需要访问外围类对象的时候,应该使用静态内部类。(255)

第七章:异常

  • 异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。(265)

  • 在Java程序设计语言中,异常对象都是派生于Throwable类的一个实例。(266)
    在这里插入图片描述
            Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。
            由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常:

  • Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受查(unchecked)异常,所有其他的异常称为受查(checked)异常。编译器将核查是否为所有的受査异常提供了异常处理器。(267)

  • 下面4种情况应该抛出异常:(267)
    1)调用一个抛出受査异常的方法,例如,FilelnputStream构造器。
    2)程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。
    3)程序出现错误,例如,a[-l]=0会抛出一个ArraylndexOutOffloundsException这样的非受查异常。
    4)Java 虚拟机和运行时库出现的内部错误。

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

  • 要想捕获一个异常,必须设置try/catch语句块。(271)
    如果在try语句块中的任何代码抛出了一个在catch子句中说明的异常类,那么
    1)程序将跳过try语句块的其余代码
    2)程序将执行catch子句中的处理器代码。
    如果在try语句块中的代码没有拋出任何异常,那么程序将跳过catch子句。
    如果方法中的任何代码拋出了一个在catch子句中没有声明的异常类型,那么这个方法就会立刻退出。

  • 通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。如果想传递一个异常,就必须在方法的首部添加一个throws说明符,以便告知调用者这个方法可能会抛出异常。这个规则也有一个例外,如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。(272)

  • 在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。

try{
	code that might throw exceptions
}catch (FileNotFoundException e){
	emergency action for missing files
}catch (UnknownHostException e){
	emergency action for unknown hosts
}catch (IOException e){
	emergency action for all other I/O problems
}
  • 不管是否有异常被捕获,finally中的代码都被执行。如果方法获得了一些本地资源,并且只有这个方法自己知道,当发生异常时,可以在finally里面回收资源。(276)
InputStream in=new FileInputStream(...);
try{
	// 1
	code that might throw exceptions
	// 2
}catch (IOException e){
	// 3
	show error message
	// 4
}finally{
	// 5
	in.close();
}
	// 6

        在上面这段代码中,有下列3种情况会执行finally子句:

  1. 代码没有抛出异常。在这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码。随后,继续执行try语句块之后的第一条语句。也就是说,执行标注的1、2、5、6处。
  2. 抛出一个在catch子句中捕获的异常。在上面的示例中就是IOException异常。在这种情况下,程序将执行try语句块中的所有代码,直到发生异常为止。此时,将跳过try语句块中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行 finally子句中的代码。
    如果catch子句没有抛出异常,程序将执行try语句块之后的第一条语句。在这里,执行标注1、3、4、5、6处的语句。
    如果catch子句抛出了一个异常,异常将被抛回这个方法的调用者。在这里,执行标注1、3、5处的语句。
  3. 代码抛出了一个异常,但这个异常不是由catch子句捕获的。在这种情况下,程序将执行try语句块中的所有语句,直到有异常被抛出为止。此时,将跳过try语句块中的剩余代码,然后执行finally子句中的语句,并将异常抛给这个方法的调用者。在这里,执行标注1、5处的语句。
  • 强烈建议解耦合try/catch和try/finally语句块。这样可以提高代码的清晰度。(277)
InputStream in=new FileInputStream(...);
try{
	try{
		code that might throw exceptions
	}finally{
		in.close();
	}
}catch (IOException e){
	show error message
}

        内层的try语句块只有一个职责,就是确保关闭输入流。外层的try语句块也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告finally子句中出现的错误。

  • 假设利用return语句从try语句块中退出。方法返回前,finally的内容将被执行。如果finally子句中也有一个return语句,这个返回值将会覆盖原始的返回值。(277)
public static int f(int n){
	try{
		return n * n;  //当n=2,被覆盖
	}finally{
		if (n = 2) return 0;
	}
}
  • 带资源的try语句的最简形式:try块退出时,会自动关闭资源。带资源的 try 语句自身也可以有 catch 子句和一个 finally 子句,这些子句会在关闭资源之后执行。原来的异常会重新抛出,而 close 方法抛出的异常会“被抑制" ,这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。 如果对这些异常感兴趣, 可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表(279)
try (Scanner in = new Scanner(new FileInputStream("...")):
	 PrintWriter out = new PrintWriter("out.txt")){
	while (in.hasNext()){
		out.println(in.next().toUpperCase());
	}
	...
} catch (...) {
	...
} finally {
	...
}
  • 使用异常机制的技巧(277)
  1. 异常处理不能代替简单的测试。因此使用异常的基本规则是:只在异常情况下使用异常机制。
  2. 不要过分地细化异常。如下面这个例子:
PrintStream out;
Stack s;
for (i = 0;i < 100; i++){
	try{
		n = s.pop();
	}catch (EmptyStackException e){
		// stack was empty
	}
	try{
		out.writeInt(n);
	}catch (IOException e){
		// problem writing to file
	}
}

        这种编程方式将导致代码量的急剧膨胀。首先看一下这段代码所完成的任务。在这里, 希望从栈中弹出100个数值,然后将它们存入一个文件中。如果栈是空的,则不会变成非空状态;如果文件出现错误,则也很难给予排除。出现上述问题后,这种编程方式无能为力。因此,有必要将整个任务包装在一个try语句块中,这样,当任何一个操作出现问题时,整个任务都可以取消。

try{
	for (i = 0; i < 100; i++){
		n = s.pop();
		out.writelnt(n);
	}
}catch (IOException e){
	// problem writing to file
}catch (EmptyStackException e){
	//stack was empty
}
  1. 利用异常层次结构。应该寻找更加适当的子类或创建自己的异常类。
    不要只抛出RuntimeException异常,应该寻找更加适当的子类或创建自己的异常类。不要只捕获Thowable异常,否则,会使程序代码更难读、更难维护。将一种异常转换成另一种更加适合的异常时不要犹豫。

第八章:泛型

  • 泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。例如,一个ArrayList类可以聚集任何类型的对象,所以不需要为不同的对象分别设计不同的类。(309)
  • 类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T (需要时还可以用临近的字母U和S)表示“任意类型”。(311)
  • 用具体的类型替换类型变量就可以实例化泛型类型(312)
public class Study {
	static class Pair<T>{
		private T first;
		private T second;
		
		public Pair() { this.first=null; this.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 first) { this.first = first; }
		
		public void setSecond(T second) { this.second = second; }
	}
	
	public static Pair<String> minmax(String[] a) {
		if(a==null||a.length==0) return null;
		String min=a[0];
		String max=a[0];
		for(int i=1;i<a.length;i++) {
			if(min.compareTo(a[i])>0) min=a[i];
			if(max.compareTo(a[i])<0) max=a[i];
		}
		return new Pair<>(min,max);
	}
	
	public static void main(String[] args) {
		String[] word= {"Mary","had","a","little","lamb"};
		Pair<String> mm=minmax(word);
		System.out.println("min= "+mm.getFirst());
		System.out.println("max= "+mm.getSecond());
	}
}

运行结果:
min= Mary
max= little

  • 可以定义一个带有类型参数的方法。泛型方法可以定义在普通类中,也可以定义在泛型类中。类型变量放在修饰符(这里是public static)的后面,返回类型的前面。(313)
public static <T> T getMiddle(T... a){
	return a[a.length / 2];
}
  • 有时,类或方法需要对类型变量加以约束,可以通过对类型变量T设置限定(bound)实现这一点:(314)
public static <T extends Comparab1e> T min(T[] a)
  • 在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。一个类型变量或通配符可以有多个限定。T extends Comparable & Serializable(315)
  • 虚拟机没有泛型类型对象----所有对象都属于普通类。无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变M, 并替换为第一个限定类型(无限定的变量用 Object。T是一个无限定的变量)(316)
  • 泛型的约束与局限性(320)
  1. 不能用基本类型实例化类型参数
            不能用类型参数代替基本类型。因此,没有Pair<double>, 只有Pair<Double> 。其原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。
  2. 运行时类型查询只适用于原始类型
            虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。试图查询一个对象是否属于某个泛型类型时,倘若使用instanceof会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
    if (a instanceof Pair<String>) // Error
    if (a instanceof Pair<T>) // Error
            同样的道理,getClass方法总是返回原始类型。
    Pair<String> stringPair = . . .
    Pair<Employee> employeePair = . . .
    if (stringPair.getClass() == employeePair.getClass()) // they are equal
            其比较的结果是 true, 这是因为两次调用 getClass 都将返回 Pair.class。
  3. 不能创建参数化类型的数组,Java不支持泛型类型的数组
    Pair<String>[] table = new Pair<String>[10]; // Error
            擦除之后,table的类型是Pair[],可以把它转换为 Object[]:
    Object[] objarray = table;
            数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个 ArrayStoreException 异常:objarray[0] = “Hello”; // Error component type is Pair
  4. 不能实例化类型变量。不能构造泛型数组。不能使用像new T(…),new T[…]或T.class这样的表达式中的类型变量
  5. 不能在静态域或方法中引用类型变量。禁止使用带有类型变量的静态域和方法
public class Singleton<T>{
	private static T singlelnstance; // Error
	public static T getSinglelnstance(){ // Error
		if (singleinstance == null)
			construct new instance of T
		return singlelnstance;
	}
}
  1. 不能抛出或捕获泛型类的实例

第九章:集合

在这里插入图片描述
在这里插入图片描述

  • 从概念上讲,Java迭代器指向两个元素之间的位置。

第十四章:并发

  • 一个程序同时执行多个任务。通常,每一个任务称为一个线程(thread),它是线程控制的简称。可以同时运行一个以上线程的程序称为多线程程序(multithreaded)。(624)
  • 不推荐使用继承Thread类实现多线程。如果有很多任务,要为每个任务创建一个独 立的线程所付出的代价太大了。可以使用线程池来解决这个问题。(630)
  • interrupt方法可以用来请求终止线程。当对一个线程调用interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的boolean标志。每个线程都应该不时地检査这个标志,以判断线程是否被中断。Thread.currentThread().islnterrupted()(633)
    如果线程被阻塞,就无法检测中断状态。当在一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被InterruptedException异常中断。
  • 线程可以有如下 6 种状态:(635)
    •New (新创建)•Runnable (可运行)•Blocked (被阻塞)•Waiting (等待)•Timed waiting (计时等待)•Terminated (被终止)
  • 新创建:当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。(635)
  • 可运行:在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(636)
  • 运行中的线程被中断,目的是为了让其他线程获得运行机会。
  • 当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。(636)
  • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进人阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。(636)
  • 有几个方法有一个超时参数。调用它们导致线程进人计时等待(timed waiting)状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep和Object.wait、Thread.join、Lock,tryLock以及Condition.await 的计时版。(636)
  • 线程因如下两个原因之一而被终止:1. 因为 run 方法正常退出而自然死亡。2. 因为一个没有捕获的异常终止了run方法而意外死亡。
    在这里插入图片描述
  • 在Java程序设计语言中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设置为在MIN_PRIORITY(在Thread类中定义为1)与MAX_PRIORITY(定义为 10)之间的任何值。NORM_PRIORITY被定义为5。每当线程调度器有机会选择新线程时, 它首先选择具有较高优先级的线程。注意死锁饿死。(638)
  • 可以通过调用t.setDaemon(true);将线程转换为守护线程(daemon thread),这一方法必须在线程启动之前调用。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。(639)
  • ReentrantLock保护代码块的基本结构如下:(646)
myLock.lock(); // a ReentrantLock object
try{
	critical section
}finally{
	myLock.unlock()// make sure the lock is unlocked even if an exception is thrown
}

在这里插入图片描述
        这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
        把解锁操作括在finally子句之内是至关重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其他线程将永远阻塞。

  • 每一个对象都有自己的锁,当不同线程操纵不同的对象时,线程之间不会相互影响。
  • 锁是可重入的, 因为线程可以重复地获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
    例如transfer方法调用的getTotalBalance方法,这也会封锁bankLock对象,此时 bankLock对象的持有计数为2。当getTotalBalance方法退出的时候,持有计数变回1。当transfer方法退出的时候,持有计数变为 0。线程释放锁。
  • 注意不能使用下面这样的代码,当前线程完全有可能在成功地完成测试,且在调用 transfer 方法之前将被中断。
if (bank.getBalance(from) >= amount){
	// thread might be deactivated at this point
	bank.transfer(from, to, amount);
}
  • 一个线程调用await方法,它进入该条件的等待集,con.await()。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。con.signalAll();这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继续执行。(649)
  • 另一个方法signal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用 signal,那么系统就死锁了。(650)
  • Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。(653)
public synchronized void method(){
	method body
}
等价于
public void method(){
	this.intrinsidock.1ock();
	try{
		method body
	}finally{
		this.intrinsicLock.unlock();
	}
}

        wait方法添加一个线程到等待集中,notifyAll /notify方法解除等待线程的阻塞状态。换句话说,调用wait或notityAll等价于con.await();或con.signalAll();

class Bank{
	private double[] accounts;
	public synchronized void transfer(int from,int to, int amount) throws InterruptedException{
		while (accounts[from] < amount)
			wait(); // wait on intrinsic object lock's single condition
		accounts[from] -= amount ;
		accounts[to] += amount ;
		notifyAll()// notify all threads waiting on the condition
	}
	public synchronized double getTotalBalance() { . . . }
}
  • 将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。例如,如果Bank类有一个静态同步的方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。(654)
  • 内部锁和条件存在一些局限:(654)
    不能中断一个正在试图获得锁的线程。
    试图获得锁时不能设定超时
    每个锁仅有单一的条件,可能是不够的。
  • 当线程进入如下形式的同步阻塞,于是它获得Obj的锁:(656)
synchronized (lock){ // this is the syntax for a synchronized block
	critical section
}

        lock对象被创建仅仅是用来使用每个 Java 对象持有的锁,可以用this。

  • 假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为 volatile。(659)
  • 线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情。(665)
if (myLock.tryLock()){
	// now the thread owns the lock
	try { . . . }
	finally { myLock.unlock(); }
}else
	// do something else
  • 如果调用带有用超时参数的tryLock,那么如果线程在等待期间被中断,将抛出 InterruptedException异常。这是一个非常有用的特性,因为允许程序打破死锁。(665)
  • 下面是使用读/写锁的必要步骤:(666)
  1. 构造一个ReentrantReadWriteLock对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():
  1. 抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
  1. 对所有的获取方法加读锁
public double getTotalBalance(){
	readLock.lock()try { . . . }
	finally { readLock.unlock(); }
}
  1. 对所有的修改方法加写锁
public void transfer(. . .){
	writeLock.lock();
	try { . . . }
	finally { writeLock.unlock(); }
}

        Lock readLock()得到一个可以被多个读操作共用的读锁,但会排斥所有写操作。
        Lock writeLock()得到一个写锁,排斥所有其他的读操作和写操作。

  • stop方法终止所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如,假定TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转人目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。(667)
            当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止。
  • suspend也方法被弃用了。suspend不会破坏对象。但是,如果用suspend挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。(667)
  • 对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插人元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。(668)
            下面的程序展示了如何使用阻塞队列来控制一组线程。程序在一个目录及它的所有子目录下搜索所有文件,打印出包含指定关键字的行。
import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public class test {
	private static final int FILE_QUEUE_SIZE = 10;
	private static final int SEARCH_THREADS = 100;
	private static final File DUMMY = new File("");
	private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE);

	public static void main(String[] args) {
		try (Scanner in = new Scanner(System.in)) {
			System.out.print("Enter base directory (e.g. /opt/jdkl.8.0/src): ");
			String directory = in.nextLine();
			System.out.print("Enter keyword (e.g. volatile): ");
			String keyword = in.nextLine();
			Runnable enumerator = () -> {
				try {
					enumerate(new File(directory));   //搜索所有的文件
					queue.put(DUMMY);   //结束标记
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
			new Thread(enumerator).start();
			for (int i = 1; i <= SEARCH_THREADS; i++) {
				Runnable searcher = () -> {   //多线程搜索关键字
					try {
						boolean done = false;
						while (!done) {
							File file = queue.take();
							if (file == DUMMY) {
								queue.put(file);
								done = true;
							} else
								search(file, keyword);
						}
					} catch (IOException e) {
						e.printStackTrace();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				};
				new Thread(searcher).start();
			}
		}
	}

	/**
	 * Recursively enumerates all files in a given directory and its subdirectories.
	 * 
	 * @paran directory the directory in which to start
	 */
	public static void enumerate(File directory) throws InterruptedException {
		File[] files = directory.listFiles();
		for (File file : files) {
			if (file.isDirectory())
				enumerate(file);
			else
				queue.put(file);
		}
	}

	/**
	 * Searches a file for a given keyword and prints all matching lines.
	 * 
	 * @param file    the file to search
	 * @param keyword the keyword to search for
	 */
	public static void search(File file, String keyword) throws IOException {
		try (Scanner in = new Scanner(file, "UTF-8")) {
			int lineNumber = 0;
			while (in.hasNextLine()) {
				lineNumber++;
				String line = in.nextLine();
				if (line.contains(keyword))
					System.out.printf("56s:%d:%s%n", file.getPath(), lineNumber, line);
			}
		}
	}
}

    生产者线程枚举在所有子目录下的所有文件并把它们放到一个阻塞队列中。这个操作很快,如果没有上限的话,很快就包含了所有找到的文件。
    我们同时启动了大量搜索线程。每个搜索线程从队列中取出一个文件,打开它,打印所有包含该关键字的行,然后取出下一个文件。我们使用一个小技巧在工作结束后终止这个应用程序。为了发出完成信号,枚举线程放置一个虚拟对象到队列中(DUMMY)当搜索线程取到这个虚拟对象时,将其放回并终止。

第二章:输入与输出(卷二)

  • 在Java API中,可以从其中读入一个字节序列的对象称作输入流,二可以向其中写入一个字节序列的对象称作输出流。这些字节序列的来源和目的地可以是文件,而且通常是文件,但是也可以是网络连接,甚至是内存块。抽象类InputStream和OutputStream构成了输入/输出(I/O)类层次结构的基础(39)
    在这里插入图片描述
  • 根据它们的所有方法来进行划分,这样就形成了处理字节字符的两个单独的层次结构。InputStream和OutputStream类可以读写单个字节或字节数组。
    在这里插入图片描述
  • 对于文本,可以使用抽象类Reader和Writer的子类。
    在这里插入图片描述
  • FileInputStream和FileOutputStream可以提供附着在一个磁盘文件的输入流和输出流,而你只需向其构造器提供文件名或文件的完整路径名。这种类只支持在字节级别上的读写,只能从fin对象中读入字节和字节数组。(46)
FileInputStream fin = new FileInputStream("employee.dat");
byte b = (byte) fin.read();

        DataInputStream只能读入数值类型。
        FileInputStream没有任何读入数值类型的方法,而DataInputStream没有任何从文件中获取数据的方法。为了从文件中读入数字,首先需要创建一个FileInputStream,然后将其传递给DataInputStream的构造器。

FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream din = new DataInputStream(fin);
double x = din.readDouble();

        可以通过嵌套过滤器来添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的,也就是说,每个对read的调用都会请求操作系统再分发一个字节。相比之下,请求一个数据块并将其置于缓冲区会显得更加高效。如果我们想使用缓存机制,以及用于文件的数据输入方法:

DataInputStream din = new DataInputStream(
	new BufferedInputStream(
		new FileInputStream("employee.dat")));
  • 在Java内部使用UTF-16编码方式。(49)
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值