Live2D

《Java核心技术·卷Ⅰ:基础知识(原版10》学习笔记 第5章 继承

《Java核心技术·卷Ⅰ:基础知识(原版10》学习笔记 第5章 继承

5.1 类、超类和子类

5.1.1 定义子类

关键字 extends表示继承

public class Manager extends Employee
{
    添加方法和域
}

关键字extends表明正在构建的新类派生于一个已存在的类。已存在的类称为 超类、基类或父类;新类称为 子类、派生类或孩子类

5.1.2覆盖方法

子类提供一个新的方法来覆盖超类中的方法:

public class Manager extends Employee
{
	public double getSalary()
		{
			double baseSalary = super.getSalary(); //super调用超类的方法
			return baseSalary + bonus;
		}
}

子类的方法不能直接访问超类的私有域

super 调用分类的方法

在子类可以 增加域、增加方法或覆盖超类的方法,然而绝对不能 删除继承的任何域和方法。

5.1.3子类构造器

public Manager(String name,double salary,int year,int month,int day)
{
	super(name,salary,year,month,day);
	bonus = 0;
}

这里关键字 super具有不同的含义。

语句

super(name,salary,year,month,day);

是”用超类Employee中含有name,salary,year,month和day参数的构造器“简写形式

由于子类的构造器不能访问父类的私有域,所以必须利用父类的构造器对这部分私有域进行初始化,我们可以通过super实现对父类构造器的调用。使用 super 调用构造器的语句必须是子类构造器的第一条语句

如果子类构造器没有显式地调用父类的构造器,则将自动调用父类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且子类的构造器中又没有显式地调用父类的其他构造器,则Java编译器将报告错误。

下面程序展现了Employee对象与Manager对象在薪水计算上的区别。

/**
 * This program demonstrates inheritance
 * @version 2022-01-16
 * @author zengzhicheng
 *
 */
public class ManagerTest {

	public static void main(String[] args) {
		Manager boss = new Manager("Carl Cracker",80000,1987,12,15);
		boss.setBonus(5000);
		
		Employee[] staff = new Employee[3];
		
		staff[0] = boss;
		staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
		staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
		
		for(Employee e:staff)
			System.out.println("name="+e.getName()+",salary="+e.getSalary());

	}

}

import java.time.LocalDate;

public class Employee {
	private String name;
	private double salary;
	private LocalDate hireDay;
	
	public  Employee(String name,double salary,int year,int month,int day) 
	{
		this.name = name;
		this.salary = salary;
		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) 
	{
		double raise = salary * byPercent / 100;
		salary += raise;
	}
}
public class Manager extends Employee
{
	private double bonus;
	
	/**
	 * @param name  是employee的name
	 * @param salary the salary 
	 * @param year the hire year
	 * @param month  the hire month
	 * @param day  the hire day
	 */
	public Manager(String name,double salary,int year,int month,int day)
	{
		super(name,salary,year,month,day);
		bonus = 0;
	}
	
	public double getSalary()
	{
		double baseSalary = super.getSalary();
		return baseSalary + bonus;
	}
	
	public void setBonus(double b)
	{
		bonus =b;
	}
	
}

5.1.4 继承的层次

由一个公共父类派生出来的所有类的集合被称为 继承层次;

在继承层次中,从某个特定的类到其祖先的路径被称为该类的 继承链

Java不支持多继承

5.1.5 多态

多态:一个对象变量可以指示多种实际类型的现象

将一个子类对象赋值给父类变量

Employee e;
e = new Employee(...);
e = new Manager(...);

在Java程序设计语言中,对象变量是 多态的

一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类对象;

不能将一个父类的引用变量赋给子类变量

在Java中,子类数组的引用可以转换为超类数组的引用,而不需要采用强制类型转换

5.1.6 理解方法调用

假设要调用x.f(args),隐式参数x声明为类c的一个对象。下面是调用过程的详细描述:

1)编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为

f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性

为public且名为f的方法(父类的私有方法不可访问)。

至此,编译器已获得所有可能被调用的候选方法。

2)接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方

法。这个过程被称为重载解析(overloading resolution)。例如,对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int)。由

于允许类型转换(int可以转换成double, Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参

数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。

至此,编译器已获得需要调用的方法名字和参数类型。

方法的名字和参数列表称为方法的 签名

覆盖方法时,一定要保证返回类型的兼容性

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

静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们列举的示例

中,编译器采用动态绑定的方式生成一条调用f (String)的指令。

4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是

D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的父类中寻找f(String),以此类推。

每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。

动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个Executive类的对象,就会自动地调用Executive.getSalary()方法。

5.1.7 阻止继承:final类和方法

不允许扩展的类被称为final类

假设希望阻止人们定义Executive类的子类,就可以在定义这个类的时候,使用final类修饰符声明。

声明格式如下:

public final class Executive extends Manager
{
    ....
}

类中特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法。(final类中所有方法自动地成为final方法)。

例如:

public class Employee
{
    ...
        public final String getName()
    {
        return name;
    }
}

5.1.8 强制类型转换

double x = 3.405;
intnx = (int) x;

对象引用的转换语法与数值表达式的类型转换类似,仅需要一对圆括号将目标类名包括起来,并放置在需要转换的对象引用之前就可以了。例如:

Manager boss = (Manager)  staff[0];

进行类型转换的唯一原因是:在暂时忽略对象的实际类型之后,使用对象的全部功能

1.向下转型,来访问子类扩展的方法。

2.向上转型,来访问父类的成员变量。

在进行类型转换之前,先查看一下是否能够成功地转换。这个过程简单使用 instanceof 操作符就可以实现。例如:

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

如果这个类型转换不成功,编译器就不会进行这个转换。

综上所述:

1.只能在继承层次内进行类型转换。

2.在将父类转换为子类之前,应当使用 instanceof 进行检查。

5.1.9 抽象类

使用 abstract 关键字,声明抽象类和抽象方法

为了提高程序的清晰度,包含一个或多抽象方法类本身必须被声明为抽象的。

public abstract class Person
{
    ...
     public abstract String getDescription();
}

除了抽象方法之外,抽象类还可以包含具体数据和具体方法。

public abstract class Person 
{
	public abstract String getDescription();
	private String name;
	
	public Person(String name)
	{
		this.name = name;
	}
	
	public String getName()
	{
		return name;
	}

}

尽量将通用的域和方法(不管是否抽象的)放在父类(不管是否为抽象类)中。

抽象方法仅充当着占位符的角色,他的具体实现在子类中。

类即使不含抽象方法,也可以将类声明为抽象类。

5.1.10 受保护访问

1)仅对本类可见—— private

  1. 对所有类可见—— public

  2. 对本包和所有子类可见——protected

4)对本包可见——默认,不需要修饰符

5.2 Object: 所有类的超类

Object 类是 java 中所有类的始祖,在 java 中每个类都是由它扩展而来的。

可以用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker",35000);

当然,Object 类型的变量只能用于作为各种值的通用支持者。

在Java中,只有基本类型(primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。

所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。

Employee[]  staff = new Emplyee[10];
obj = staff; //ok
obj = new int[10]; //ok

5.2.1 equals方法

equeals 方法:Object 类中用于检测一个对象是否等于另外一个对象。

在 Object 类中,这个方法判断两个对象是否具有相同的引用。如果两个对象具有相同的引用那么他们一定相等。

在检测中只有两个对象属于同一个类时,才有可能相等。

在子类定义equals方法时,首先调用父类的equals。如果检测失败,对象就不可能相等。如果父类中的域都相等,就需要比较子类的实例域。

public class Manager extends Employee
{
    '''
        public boolen equals(Object otherObject)
    {
        if(!super.equals(otherObject)) return false;
        
        Manager other =  (Manager) othetObject;
        return bonus == other.bonus;
	}
}

5.2.2 相等测试域继承

Java语言规范要求equals方法具有下面的特性

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 。

这些规则十分合乎情理,从而避免了类库实现者在数据结构中定位一个元素时还要考虑调用x.equals(y),还是调用y.equals(x)的问题。

如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。
如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。

对象之间编写equals方法的步骤:

1.比较 this == otherObject

2.检测otherObject == null

3.比较this和otherObject是否同一个类的对象,this.getClass == otherObject.getClass() 或者判断otherObject是否为某类的子类

4.将otherObject转成相应的类类型变量

5.开始对所有需要比较的属性进行比较了,基本类型使用== 属性为对象使用Object.equals()比较

对于数组类型的域,可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等

5.2.3 hashCode 方法

hash 是散列的意,就是把任意长度的输入,通过散列算法变换成固定长度的输出,该输出就是散列值。

1.如果散列表中存在和散列原始输入K相等的记录,那么K必定在f(K)的存储位置上;

2.不同关键字经过散列算法变换后可能得到同一个散列地址,这种现象称为碰撞;

3.如果两个Hash值不同(前提是同一Hash算法),那么这两个Hash值对应的原始输入必定不同。

HashCode,散列码,关键点如下

  1. HashCode 的存在主要是为了查找的快捷性,HashCode 是用来在散列存储结构中确定对象的存储地址的;

2.如果两个对象equals相等,那么这两个对象的 HashCode一定也相同;

3.如果对象的equals方法被重写,那么对象的HashCode方法也尽量重写;

4.如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置

散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果x和y是两个不同的对象,x.hashCode( )与y.hashCode( )基本上不会相同。

String 类使用下列算法计算散列码:

int hash = 0;
for (int i = 0 ; i<length();i++)
    hash = 31 * hash + charAt(i);

如果重新定义 equals 方法,就必须定义 hashCode 方法,以便用户可以将对象插入到散列表中。

Equals 与 hashCode 的定义必须一致:如果 x.equals(y) 返回 true ,那么 x.hashCode( ) 就必须与 y.hashCode( ) 具有相同的值。例如,如果用定义的 Employee.equals 比较雇员的ID,那么 hashCode 方法就需要散列 ID ,而不是雇员的姓名或存储地址。

如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。

int hashCode()

返回对象的散列码。散列码可以是任意的整数,包括正数或负数。两个相等的对象要求返回相等的散列码。

static int hash(Object... object)

返回一个散列码,由提供的所有对象的散列码组合而得到

5.2.4 toString 方法

在 Object 中还有一个重要的方法,就是 toString 方法,它用于返回表示对象值的字符串。下面是一个典型的例子。Point类的toString方法将返回下面这样的字符串:

java.awt.Point[x=10,y=20]

绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。下面是Employee类中的toString 方法的实现:

public String toString()
{
    return getClass().getName()
        +"[name=" + name
        +",salary = " +salary
        ",hireDay=" + hireDay
        +"]";
}

to.String 方法也可以供子类调用。

当然,设计子类的程序员也应该定义自己的 toString 方法,并将子类域的描述添加进去。如果超类使用了 getClass( ).getName( ),那么子类只要调用super.toString( )就可以了。例如,下面是Manager类中的 toString 方法:

public class Manager extends Employess
{
    '''
        public String toString()
    {
        return super.toString()
            + "[bonus" +bonus
            + "]";
    }
}

现在将 Manager 对象打印输出内容:

Manager[name=...,salary=...,hireDay=...][bonus=...]

随处可见 toString 方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java 编译就会自动地调用 toString 方法,以便获得这个对象的字符串描述。例如,

Point p = new Ponit(10,20);
String message = "The current position is" + p;

在调用 x.toString( ) 的地方可以用""+x替代。这条语句将一个空串与 x 的字符串表示相连接。这里的 x 就是 x.toString( ) 。与 toString 不同的是,如果 x 是基本类型,这条语句照样能够执行。

Class getClass()

返回包含对象信息的类对象。稍后会看到Java提供了类运行时的描述,它的内容被封装在Class类中。

boolean equals(Object otherObject)

比较两个对象是否相等,如果两个对象指向同一块存储区域,方法返回true;否则方法返回false。在自定义的类中,应该覆盖这个方法。

String toString()

返回描述该对象值的字符串。在自定义的类中,应该覆盖这个方法。

Sring getName()

返回这个类的名字。

Class getSuperclass()

以Class对象的形式返回这个类的父类信息。

5.3 泛型数组列表

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

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

当然这段代码并没有完全解决运行时动态更改数组的问题。一旦确定了数组大小,改变他就太不容易了。

在 java 最简单的方法是使用 Java 中另外被称为 ArrayList 的类。它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

ArrayList 是一个采用类型参数泛型类。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如,ArrayList

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

这也被称为菱形语法。

使用add方法可以将元素添加到数组列表中。例如,下面展示了如何将雇员对象添加到数组列表中的方法:

staff.add(new Employee("Harry Hacker",...));
staff.add(new Employee("Tony Tester",...));

数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力:如果调用 add 且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

数组列表的容量与数组的大小有一个非常重要的区别。如果为数组分配100个元素的存储空间,数组就有100个空位置可以使用。而容量为100个元素的数组列表只是拥有保存100个元素的潜力(实际上,重新分配空间的话,将会超过100),但是在最初,甚至完成初始化构造之后,数组列表根本就不含有任何元素。

size 方法将返回数组列表中包含的实际元素数目。例如,

staff.size()

将返回staff数组列表的当前元素数量,它等价于数组 a 的 a.length 。

一旦能够确认数组列表的大小不再发生变化,就可以调用 trimToSize 方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。

一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时,再调用trimToSize。

ArrayList<E>();

构造一个空数组列表.

ArrayList<E>(int initialCapacity)

用指定容量构造一个空数组列表。
参数:initalCapacity 数组列表的最初容量

boolean add(E obj)

在数组列表的尾端添加一个元素。永远返回true。
参数:obj 添加的元素

int size()

返回存储在数组列表中的当前元素数量。(这个值将小于或等于数组列表的容量。)

void ensureCapacity(int capacity)

确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素。
参数:capacity 需要的存储容量

void trimToSize()

将数组列表的存储容量削减到当前尺寸

5.3.1 访问数组列表元素

使用 ge t和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的[ ]语法格式。
例如,要设置第i个元素,可以使用:

staff.set(i,harry);

他等价于对数组a的元素赋值(数组 的下表从0开始);

只有i小于或等于数组列表的大小时,才能够调用list.set(i, x)。例如,下面这段代码是错误的:

ArrayList<Employee> staff = new ArrayList<>(100);
list.set(0,x);

使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中已经存在的元素内容。

使用下列格式获取数组列表的元素:

Employee e = staff.get(i);

下面这个技巧可以一举多得,既可以灵活地扩展数组,又可以方便地访问数组元素。

首先,创建一个数组,并添加所有的元素:

ArrayList<X> list = new ArrayList<>();
while (...)
{
    x = ...;
    list.add(x);
}

执行完上述操作后,使用toArray方法将数组元素拷贝到一个数组中。

X[] a =  new X[list.size()];
list.toArray(a);

除了在数组列表的尾部追加元素外,还可以在数组列表的中间插入元素,使用带索引参数的add方法。

int n = staff.size()/2;
staff.add(n,e);

为了插入一个新元素,位于n之后的所有元素都要向后移动一个位置。如果插入新元素后,数组列表的大小超过了容量,数组列表就会被重新分配存储空间。

同样地,可以从数组列表中间删除一个元素。

Employee e = staff.remove(n);

位于这个位置之后的所有元素都向前移动一个位置,并且数组的大小减1。

可以使用“ for each ” 循环遍历数组列表:

for (Employee e : staff)
	do something with e;
import java.util.ArrayList;

public class ArrayListTest {

	public static void main(String[] args) {
		ArrayList<Employee> staff = new ArrayList<>();
		
		staff.add(new Employee("Carl Cracke",75000,1987,12,15));
		staff.add(new Employee("Harry Hacker",50000,1989,10,1));
		staff.add(new Employee("Tony Tester",40000,1990,3,15));
		
		for(Employee e:staff)
			e.raiseSalary(5);
		
		for (Employee e :staff)
			System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());

	}

}

void set(int index)

设置数组列表指定位置的元素值,这个操作将覆盖这个位置的原有内容。
参数:index 位置(必须介于0~size()-1之间)

e get(int index)

获得指定位置的元素值。
参数:index 获得的元素位置(必须介于0~size()-1之间)

void add(int index,E obj)

向后移动元素,以便插入元素

E remove(int index)

删除一个元素,并将后面的元素向前移动。被删除的元素由返回值返回。
参数:index 被删除的元素位置(必须介于0~size()-1之间

5.3.2 类型化与原始数组列表的兼容性

在这一节学习了如何与没有使用类型参数的遗留代码交互操作

假设有下面这个遗留下来的类:

public class EmployeeDb
{
    public void update(ArrayList list){...}
    public ArrayList find(String query){...}
   
}

可以将一个类型化的数组列表传递给update方法,而不需要进行任何类型转换。

ArrayList<Employee> staff = ...;
employeeDB.update(staff);

也可以将staff对象传递给update方法。

使用类型转换并不能避免警告。

一旦能确保不会造成严重的后果,可以用 @SuppressWarnings("unchecked") 标注来标记这个变量能够接受类型转换,如下所示:

@SuppressWarnings("unchecked") ArrayList<Employee> result = 
    (ArrayList<Employee> employeeDB.find(query));

5.4 对象包装器与自动装箱

有时,需要将 int 这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int 。通常,这些类称为包装器(wrapper)。这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character、Void和 Boolean(前6个类派生于公共的超类Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。

假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成 ArrayList 。这里就用到了Integer 对象包装器类。我们可以声明一个 Integer 对象的数组列表

ArrayList<Integer> list = new ArrayList<>()

幸运的是,有一个很有用的特性,从而更加便于添加int类型的元素到ArrayList中。下面这个调用

list.add(3);

将自动地变成

list.add(Integer.valueOf(3));

这种变换被称为 自动装箱

相反地,当将一个Integer对象赋给一个int值时,将会自动地拆箱。也就是说,编译器将下列语句:

int n = list.get(i);

翻译成

int n = list.get(i).intValue();

在算术表达式中也能自动装箱和拆箱

两个包装器对象比较时调用 equals 方法

强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。

使用数值对象包装器还有另外一个好处。Java设计者发现,可以将某些基本方法放置在包装器中,例如,将一个数字字符串转换成数值。
要想将字符串转换成整型,可以使用下面这条语句:

int x = Integer.parseInt(s);

这里与 Integer 对象没有任何关系,parseInt 是一个静态方法。但 Integer 类是放置这个方法的一个好地方。

int intValue()

以int的形式返回Integer对象的值(在Number类中覆盖了intValue方法)。

static String toString(int i)

以一个新String对象的形式返回给定数值i的十进制表示。

static String toString(int i,int radix)

返回数值 i 的基于给定 radix 参数进制的表示。

static int parseInt(String s)

static int parseInt(String s,int radix)

返回字符串s表示的整型数值,给定字符串表示的是十进制的整数(第一种方法),或者是radix参数进制的整数(第二种方法)

static Integer valueOf(String s)

Static Integer value Of(String s,int radix)

返回用s表示的整型数值进行初始化后的一个新Integer对象,给定字符串表示的是十进制的整数(第一种方法),或者是radix参数进制的整数(第二种方法)

Number parse(String s)

返回数字值,假设给定的String表示了一个数值。

5.5 参数数量可变的方法

用户自己也可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。

下面是一个简单示例:其功能是计算若干个数值的最大值。

public static double max(double... values)
{
    double largest = Double.NEGATIVE_INFINITY;
    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 方法。

5.6 枚举类

例子:

public enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE};

实际上,这个声明定义的类型是一个类,它刚好有4个实例,在此尽量不要构造新对象。

因此,在比较两个枚举类型的值时,永远不需要调用 equals ,而直接使用“= =”就可以了。

如果需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。下面是一个示例:

public enum Size
{
    SAMLL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
    
    private String abbreviation;
    
    private Size(String abbreviation) {this.abbreviation = abbreviation;}
    public String getAbbreviation(){return abbrevation;}
}

所有的枚举类型都是Enum类的子类。它们继承了这个类的许多方法。其中最有用的一个是 toString ,这个方法能够返回枚举常量名。例如,Size.SMALL.toString( ) 将返回字符串 “SMALL” 。

toString的逆方法是静态方法valueOf。例如,语句:

Size s = Enum.valueOf(Size.class,"SMALL");

将s设置成了Size.SMALL

import java.util.Scanner;

public class EnumTest {

	public static void main(String[] args) {
		Scanner in = new Scanner(System.in);
		System.out.print("Enter a size:(SMALL.MEDIUM,LARGE,EXTRA_LARGE)");
		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.EXTRA_LARGE)
		{
			System.out.println("Good job--you paid attention to the _.");
		}
		

	}

}

enum Size
{
	SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");
	
	private String abbreviation;
    
    private Size(String abbreviation) {this.abbreviation = abbreviation;}
    public String getAbbreviation(){return abbreviation;}
}

image-20220118140440997

5.7 反射

反射库(reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵 Java 代码的程序。这项功能被大量地应用于 JavaBeans 中,它是Java组件的体系结构。使用反射,Java 可以支持 Visual Basic 用户习惯使用的工具。特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。

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

反射机制可以用来:

  1. 在运行时分析类的能力。
  2. 在运行时查看对象,例如,编写一个 toString 方法供所有类使用。
  3. 实现通用的数组操作代码。
  4. 利用Method对象,这个对象很像C++中的函数指针。

反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。

5.7.1 Class类

在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。

保存这些信息的类被称为 Class ,Object 类中的 getClass( )方 法或者 Object.class 将会返回一个 Class 类型的实例。可以通过返回的 Classs 实例获取该类的全类名、动态创建该 Class 的实例。(类/实例.getClass().getName() 、类/实例.getClass().newInstance())

最常用的 Class 方法是 getName 。这个方法将返回类的名字。

System.out.println(e.getClass().getName()+""+e.getName());

还可以用静态方法 forName 获得类名对应的Class对象

String className = "java.util.Random";
Class cl = Class.forName(className);

无论何时使用这个方法,都应该提供一个异常处理器

一个类只有一个Class对象,虚拟机为每个类型管理 Class 对象,获取方法有三种:

  1. 通过对象实例获取;
    Person p = new Person();
    Class clazz = p.getClass();
  2. 通过forName()方法获取
    Class clazz = Class.forName("com.xxx.xxx.Person");
  3. 通过类名获取
    Class clazz = Person.class;

可以利用==运算符实现两个类对象比较的操作。例如,

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

还有一个很有用的方法 newInstance( ) ,可以用来动态地创建一个类的实例。例如,

e.getClass().newInstance();

创建了一个与 e 具有相同类类型的实例。newInstance 方法调用默认的构造器(没有参数的构造器)初始化新创建的对象。如果这个类没有默认的构造器,就会抛出一个异常。

5.7.2 捕获异常

可以提供一个“捕获”异常的处理器(handler)对异常情况进行处理。

异常有两种类型:未检查异常已检查异常。对于已检查异常,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问 null 引用,都属于未检查异常。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。

现在,介绍一下如何实现最简单的处理器。

将可能抛出已检查异常的一个或多个方法调用代码放在try块中,然后在catch子句中提供处理器代码。

try
{
    statements that might throw exceptions
}
catch (Exception e)
{
    handler action
}

下面是一个示例

try
{
    String name = ...;
    Class cl = Class.forName(name);
    do something wih cl
}
catch (Exception e)
{
    e.printStackTrace();
}

如果类名不存在,则将跳过 try 块中的剩余代码,程序直接进入 catch 子句(这里,利用 Throwable 类的 printStackTrace 方法打印出栈的轨迹。Throwable 是 Exception 类的超类)。如果 try 块中没有抛出任何异常,那么会跳过 catch 子句的处理器代码。

5.7.3 利用反射分析类的能力

下面简要地介绍一下反射机制最重要的内容——检查类的结构。

在 java.lang.reflect 包中有三个类 FieldMethodConstructor 分别用于描述类的方法构造器。这三个类都有一个叫做getName 的方法,用来返回项目的名称。Field 类有一个 getType 方法,用来返回描述域所属类型的 Class 对象。Method和Constructor 类有能够报告参数类型的方法,Method 类还有一个可以报告返回类型的方法。这三个类还有一个叫做 getModifier s的方法,它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外,还可以利用 java.lang.reflect 包中的Modifier 类的静态方法分析 getModifiers 返回的整型数值。例如,可以使用 Modifier 类中的 isPublic 、isPrivate 或 isFinal 判断方法或构造器是否是 public 、private 或 final 。我们需要做的全部工作就是调用 Modifier 类的相应方法,并对返回的整型数值进行分析,另外,还可以利用 Modifier.toString 方法将修饰符打印出来。

Class 类中的 getFields 、getMethods 和 getConstructors 方法将分别返回类提供的 public 域、方法和构造器数组,其中包括超类的公有成员。Class 类的 getDeclareFields 、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。

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

查看对象域的关键方法是 Field 类中的 get 方法。如果f是一个 Field 类型的对象(例如,通过 getDeclaredFields 得到的对象), obj是某个包含 f 域的类的对象,f.get(obj) 将返回一个对象,其值为obj域的当前值。

反射机制的默认行为受限于 Java 的访问控制。然而,如果一个 Java 程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用 Field、Method 或 Constructor 对象的 setAccessible 方法。例如,

f.setAccessible(true);

setAccessible 方法是 AccessibleObject 类中的一个方法,它是 Field、Method 和 Constructor 类的公共超类。这个特性是为调试、持久存储和相似机制提供的。

5.8 继承的设计技巧

1.将公共操作和域放在超类
这就是为什么将姓名域放在 Person 类中,而没有将它放在 Employee 和 Student 类中的原因。

2.不要使用受保护的域
有些程序员认为,将大多数的实例域定义为 protected 是一个不错的主意,只有这样,子类才能够在需要的时候直接访问它们。然而,protected 机制并不能够带来更好的保护,其原因主要有两点。第一,子类集合是无限制的,任何一个人都能够由某个类派生一个子类,并编写代码以直接访问 protected 的实例域,从而破坏了封装性。第二,在Java程序设计语言中,在同一个包中的所有类都可以访问proteced 域,而不管它是否为这个类的子类。
不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。

3.使用继承实现“is-a”关系
使用继承很容易达到节省代码的目的,但有时候也被人们滥用了。例如,假设需要定义一个钟点工类。钟点工的信息包含姓名和雇佣日期,但是没有薪水。他们按小时计薪,并且不会因为拖延时间而获得加薪。这似乎在诱导人们由 Employee 派生出子类 Contractor,然后再增加一个hourlyWage域。这并不是一个好主意。因为这样一来,每个钟点工对象中都包含了薪水和计时工资这两个域。在实现打印支票或税单方法的时候,会带来无尽的麻烦,并且与不采用继承,会多写很多代码。钟点工与雇员之间不属于“is-a”关系。钟点工不是特殊的雇员。

4.除非所有继承的方法都有意义,否则不要使用继承

5.在覆盖方法时,不要改变预期的行为

6.使用多态,而非类型信息

无论什么时候,对于下面这种形式的代码

if(x is of type 1)
    action1(x);
else if (x is type 2)
    action2(x);

都应该考虑使用多态性。
action1 与 action2 表示的是相同的概念吗?如果是相同的概念,就应该为这个概念定义一个方法,并将其放置在两个类的超类或接口中,然后,就可以调用

x.action();

以便使用多态性提供的动态分派机制执行相应的动作。
使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。

7.不要过多地使用反射

反射机制使得人们可以通过在运行时查看域和方法,让人们编写出更具有通用性的程序。这种功能对于编写系统程序来说极其实用,但是通常不适于编写应用程序。反射是很脆弱的,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。


总结

终于学完了这一章,花了大概三天,十二个小时左右,重新学了一遍,掌握了很多之前为了应付考试而没有掌握的知识点,但是还是需要具体项目练手,最后的反射还是有很多知识点没弄很清楚,打算看完整本书再回来看看 。接下来是两个高级主题:接口和lambda表达式;继续努力鸭ヾ(≧▽≦*)o

posted @ 2022-01-18 17:08  逝者如斯夫zeng  阅读(89)  评论(0编辑  收藏  举报