以《Java核心技术 卷1》第10版为主,结合自身实践进行截图及细节描述
一、 类,超类和子类
1~5小节是讲如何构造子类,6~7小节讲方法调用,8,9,10小节单独存在。
1. 定义子类
“is-a”关系是继承的一个明显特征。比如经理(Manager)是一个雇员(Employee),就可以推出 经理继承雇员。(实际操作中当然不会这么简单,不要滥用继承)
public class Manager extends Employee{
添加方法和域
}
继承关键字:extends,其表明正在构造的新类(Manager)派生于一个已存在的类(Employee)。已存在的类称为超类、基类或父类;新类称为子类、派生类或孩子类。子类比超类拥有的功能更加丰富。
Java中所有的继承都是公有继承。为了说明Java中公有继承的规则:请看下面例子:
package com.csdn.a;
//定义一个超类:四种访问修饰符各一种
public class Employee {
private int a =1;
int b =2;
protected int c=3;
public int d =4;
}
package com.csdn.a;
// 与超类Employee在同一个包中
public class EmployeeManager extends Employee{
{
System.out.println(a); //报错:The field Employee.a is not visible
System.out.println(b);
System.out.println(c);
System.out.println(d);
}
}
package com.csdn.b;
import com.csdn.a.Employee;
// 与超类Employee不在同一个包中
public class Manager extends Employee {
{
System.out.println(a); //报错:The field Employee.a is not visible
System.out.println(b); //报错:The field Employee.b is not visible
System.out.println(c);
System.out.println(d);
}
}
package com.csdn.c;
import com.csdn.a.EmployeeManager;
import com.csdn.b.Manager;
//定义执行类,在与父类Employee不一样的包中
public class Welcome {
public static void main(String[] args) {
Manager m1 = new Manager();
System.out.println("Manager对象"+m1.c); // 错误
System.out.println("Manager对象"+m1.d);
EmployeeManager m2 = new EmployeeManager();
System.out.println("EmployeeManager对象"+m2.b); // 错误
System.out.println("EmployeeManager对象"+m2.c); // 错误
System.out.println("EmployeeManager对象"+m2.d);
}
}
package com.csdn.a;
import com.csdn.a.EmployeeManager;
import com.csdn.b.Manager;
//定义执行类,在与父类Employee一样的包中,可顺利执行
public class Welcome {
public static void main(String[] args) {
Manager m1 = new Manager();
System.out.println("Manager对象"+m1.c);
System.out.println("Manager对象"+m1.d);
EmployeeManager m2 = new EmployeeManager();
System.out.println("EmployeeManager对象"+m2.b);
System.out.println("EmployeeManager对象"+m2.c);
System.out.println("EmployeeManager对象"+m2.d);
}
}
提炼出以下规则:
- 子类会继承父类的public,protected成员,是否继承默认成员,得看子类与父类是否在一个包中,在就继承,不在就不继承。(能被继承的成员是可以在子类中直接调用的)
- 子类对象可直接访问父类的public成员,子类是否能直接访问所继承的protected成员或者默认成员,得看构造子类对象的语句是否在父类所属的包中,在可以直接访问,不在不可以。
因为继承机制的存在,故在设计类的时候,应将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。
2. 覆盖(重写)方法
由上面一部分知道:子类会继承父类的部分方法,但有时父类中方法的具体实现并不会满足子类的需求。比如父类Employ中有getSalary(返回薪水)方法,但是子类Manager返回薪水时要再加上奖金(bonus)。因此需要重写(覆盖)方法。重写方法的要求:
- 子类只能重写父类继承过来的方法(哪些方法会被继承,看上一部分)
- 访问级别应该更高或相等(public>protected>default)
- 方法签名(方法名和参数列表)必须相同
- 返回类型必须相同,但也可是其子类
- 重写的方法不可抛出比被重写方法声明的检查异常更广的检查异常,但可以更小,甚至不抛出异常【异常的知识第6章介绍】
- final修饰的方法不能被重写
当一个方法被重写,继承过来的方法会被隐藏,当需要调用时,要加super关键字,表示此时调用父类的方法。
public double getSalary(){ //子类重写的getSalary,要在父类的基础上加上奖金bonus
double baseSalary = super.getSalary(); //获取父类薪水
return baseSalary+bonus; //加上奖金返回
}
3. 子类构造器
public Manager(String name,double salary,int year,int month,int day){
super(name,salary,year,month,day); //调用父类Employee中对应的构造器
bonus=0;
}
- 可以通过super实现对超类构造器的调用;super语句必须是子类构造器中的第一句
- 若子类构造器没有显式地调用超类构造器,会自动调用超类默认构造器(无参构造器)
- 若超类没有无参构造器,子类也没有显式调用超类构造器,则会编译错误
4. super 和 this
各自用途
this:引用隐式参数;调用该类其他构造器,调用语句在第一句。
super:调用超类的方法;调用超类构造器,调用语句在第一句。
this和super在构造器中不能同时发挥调用构造器的作用,因为它们都要求在构造器中第一句位置。
区别
super只是一个关键字,而this是一个对象的引用。举个简单的例子:
方法中:
return super; //错误
return this; //可以
5. 继承层次
由一个公共超类派生出来的所有类的集合称为继承层次,在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。
Java中只有单继承,即一个类只有一个父类,但可以有无数子类。
6. 多态
Java中,多态体现在对象变量上,即对象变量可引用本类对象,也可以引用本类的子类对象
- 可以将子类对象赋给超类变量。子类的子类也是可以的。
Employee e;
e = new Employee(...);
e = new Manager(...);
e = new Executive(...);
- 虽然对象变量是多态的,但对象本质上并未发生变化。
Manager boss = new Manager(...);
Employee[] satff = new Employee[3];
satff[0] = boss; //ok
//Manager中有setBonus方法,Employee没有该方法
boss.setBonus(5000); //ok
satff[0].setBonus(5000); //Error
//satff[0]本质仍是Employee类对象变量,所以是不可以调用setBonus方法的。
- 不可将超类引用赋给子类变量
Manager m = staff[0]; //Error
- 子类数组的引用可转换为超类数组的引用–ArrayStoreException错误
Manager[] managers = new Manager[10];
Employee[] staff = managers; //ok
// 接下来看这问题
staff[0] = new Employee(); //staff[0]本质其实是Manager引用,但编译器接受了这个赋值操作,但会运行报错:ArrayStoreException
- 强制类型转换------超类引用赋值给子类变量
在此之前,要理解一点:同个方法(重写),是调用子类的还是父类的,是看实际对象引用是子类还是父类(动态绑定)。但是父类变量是无法调用子类中的方法的,即使实际引用是子类,而强制类型转换就是暂时忽略对象的实际类型后,使用对象的全部功能。(一般情况下,尽量少用强制类型转换和instanceof关键字)
Employee em = new Manager(); //多态的应用
em.setBonus(1000); //报错,因为Employee类中是没有该方法的
Manager ma = (Employee)em; //强制类型转换
ma.setBonus(1000); //ok
//为什么可以被强制类型转换呢?因为em中本身就是Manager引用,下面语句就不对。
String s = (String )em; //强制类型转换错误
//为方便判断是否可强制类型转换,引入了instanceof关键字
if (em instanceof Manager){
.... //若em是Manager的实例,返回true,否则false。若em是null,返回false
}
7. 理解方法调用
假设调用x.f(args),隐式参数x为类C的对象。下面是调用过程的详细描述:(动态绑定)
- 编译器查看对象的声明类型和方法名。即:编译器会一一列举所有C类中名为f的方法和其从超类中继承过来的名为f的方法
- 编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析。如果编译器没有找到或者经过类型转换后与多个方法匹配,就会报告一个错误
静态绑定与动态绑定
如果是private方法,static方法,final方法或者构造器,那么编译器可以准确地知道应该调用哪个方法,将这种调用方式称为静态绑定。与此对应,若调用的方法依赖于隐式参数的实际类型,则在运行时实现动态绑定。
当采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型时D,它是C的子类。如果D类定义了方法f(String),就直接调用;否则,会在D的超类中寻找f(String),以此类推。
每次调用方法都要进行探索,时间开销相当大。因此虚拟机预先为每个类创建了一个方法表。
8. 阻止继承:final类和方法
前面也说过:final修饰变量,表示不可改变其值。
final类:不允许扩展(继承)该类,类中方法自动变为final方法,类中域可不会变
public class Executive extends Manager{ //不允许任何类extends Executive
...
}
final方法:不允许子类覆盖(重写)该方法
public final String getName(){
......
}
使用建议:
为避免动态绑定所带来的系统开销,在设计类层次时,应仔细思考应将哪些方法或类声明为final。
虚拟机的即时编译器可准确的知道类之间的继承关系,并能够检测出类中是否真正地存在覆盖给定的方法。如果方法很简单,被频繁调用而且没有被覆盖,那么即时编译器会将该方法进行内联处理。
9. 抽象类
由实践发现,其实祖先类(超类的超类…)更具有通用性,甚至抽象性。而且,有时甚至不需要祖先类实例,仅需要派生它,具体的实现由子类自身控制。为此引入了抽象类。
public abstract class Person{
......
public abstract String getDescription(); //抽象方法没有方法体
}
Person p =new Student(...); //Student继承自Person类,多态使用
- 抽象类,抽象方法关键字:abstract
- 抽象类可以有0个,1个或多个抽象方法,换言之:没有抽象方法的类可以被声明为抽象类,有抽象方法的类是必须被声明为抽象类。
- 抽象类可以包含具体数据和具体方法
- 抽象方法充当着占位的角色,没有方法体,具体实现在子类中。
- 一个类继承抽象类,要么实现其全部抽象方法,要么将自己也标注为abstract class
- 抽象类不能被实例化,即不可使用构造器,但可以利用多态定义抽象类的对象变量
利用抽象,多态以及动态绑定机制,可以有效的减少代码的输出(这句话不理解就算)
10. 受保护访问(protected)
有时希望超类中某些方法或域可被子类(任何包中的子类)访问,请将这些方法或域设置为protected。
在实际应用中,要谨慎使用protected属性,因为如果在类中设置了一些受保护域,其他程序员由此类派生出新类,一旦该类需要进行一些修改,就得通知所有使用这个类的程序员。这违背了OOP提倡的数据封装原则。但是,protected方法更具实际意义。
到此为止,已讲完4个访问修饰符。为了加深影响,将上一章表格再次呈现:
修饰符 | 本类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | ✔ | ✔ | ✔ | ✔ |
protected | ✔ | ✔ | ✔ | ✖ |
default | ✔ | ✔ | ✖ | ✖ |
private | ✔ | ✖ | ✖ | ✖ |
二、Object:所有类的超类
Object类是所有类的始祖,在Java中每个类都是由它扩展而来的。
public class Employee extends Object //不需要这样写,如果没有明确指出超类,Object类就被认为是这个类的超类
Object obj =new Employee(...); //可使用Object类型的变量引用任何类型的对象(多态)
在Java中,只有基本类型(数值,字符,布尔类型)不是对象。所有的数组类型(包括对象数组和基本类型数组)都扩展了Object类,即可多态。下面是子类可能会重写Object类的方法
1. equals方法
Object类中equals方法用于检测一个对象是否等于另外一个对象。在Object类中,equals方法是判断两个对象是否具有相同的引用。这种判断是否相等的机制合理,但有时我们更需要检测两个对象状态的相等性,所以需要重写equals方法。
public class Employee{
@Override //重写标记,若重写方法的要求不符合,就会报错。相当于一种提醒功能
public boolean equals(Object otherObject){ //重写equals方法,重写要求见上
if(this == otherObject) return true; //自己和自己比较返回true
if(otherObject==null) return false; //要比较的对象为null,返回false
if(getClass() != otherObject.getClass()) return false; //不属于同一个类,返回false
//① if(!(otherObject instanceof Employee)) return flase; //otherObject不属于Employee类对象或其子类对象,返回false
Employee other = (Employee)otherObject; //强制类型转换
// 属性比较,== 比较基本类型域,equals比较对象域
//② return name.equals(other.name) && salary== other.salary && hireDay.equals(other.hireDay); //比较属性
return Objects.equals(name,other.name) && salary== other.salary && Objects.eequals(hireDay,other.hireDay); //比较属性 - 代码优化
}
}
上面有两条注释的语句,解释一下:
① getClass() != otherObject.getClass 和 if(!(otherObject instanceof Employee)) 两种方式检测参数类之间的关系。前者判断是否属于同一类对象,后者判断对象是否是某一类的实例(此时是包括子类的)。如果子类能够拥有自己的相等概念,使用getClass方式,如果由超类决定相等的概念,使用instanceof方式检测。
② 使用Objects静态equals方法比较域对象,是将null值考虑进去的结果。Objects.equals(Object a, Object b):对象a和b两个参数都是null,返回true,有一个参数为null,返回false,都不为null,调用a.equals(b)。
在Employee子类重写equals方法时,可先调用超类的equals方法,再进行自己专属域的判断。
public class Manager extends Employee{
public boolean equals(Object otherObject){
if(!super.equals(otherObject)) return false; //先比较超类的元素
Manager other = (Manager)otherObject;
return bonus == other.bonus;
}
}
equals方法应具有下面特性
- 自反性:对于任何非空应用x,x.euqals(x),应返回true
- 对称性:对于任何引用x,y,若x.equals(y)返回true,y.equals(x)也应返回true
- 传递性:对于任何引用x,y和z,若x.equals(y)返回true,y.equals(z)返回true,x.quals(z)也应返回true
- 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应返回同样的结果
- 对于任何非空引用x,x.equals(null)应返回false
补充知识点
关于instanceof方式检测类,其实会违背了对称性原则。假设两个对象变量:Employee e;Manager m。e.equals(m),没有什么问题。但m.equals(e)可能会报错,因为e是无法强制转换为Manager 的。所以应慎重使用“instanceof” 检测类方式。
在比较域时,其实发现对域(包括私有域)的使用是直接调用,是不管和谁比较的。这是因为就是在本类中的equals方法。(如果没有此处疑惑,可略过)
数组类型的域,可使用静态Arrays.equals方法检测相应的数组元素是否相等。
2. hashCode方法
散列码(hash code)是由对象导出的一个整形值,没有规律,每个对象都有一个默认的散列码,其值为对象的存储地址。需要注意的是:字符串的散列码是由内容导出的(不可变字符串是在公共的共享池中存储)。
若重新定义equals方法,就必须重新定义hashCode方法,以便用户可将对象插入到散列表中。hashCode方法应返回一个整型数值(也可是负数),而且应合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。
hashCode重写
public class Employee{
// 简易模式
public int hashCode(){
return 7* name.hashCode()+11* new Double(salary).hashCode()+13 *hireDay.hashCode();
}
// 进化:使用Objects null安全的hashCode方法,使用基本数据类型的包装类中静态方法hashCode 来避免创建Double对象
public int hashCode(){
return 7* Objects.hashCode(name)+11* Double.hashCode(salary)+13 *Objects.hashCode(hireDay);
}
//更简便模式:使用Objects.hash方法进行多个参数的组合
public int hashCode(){
return Objects.hash(name,salary,hireDay)
}
}
- 前两种方式都是hashCode码乘一质数,是为减少hash冲突。
- equals和hashCode的定义必须一致:若x.equals(y) 返回true,则x.hashCode() == y.hashCode()
- 数组类型的域,可使用Arrays.hashCode方法计算其散列值
3. toString方法
Object类的toString方法默认打印对象所属的类名和散列码,但大部分需求是返回对象的各个域值。使用toString方法的原因是:只要对象与一个字符串通过“+”连接起来,Java编译器就会自动调用toString方法,比较方便。
// 一般格式
public String toString(){
return "Employee[name="+name+",salary="+salary+",hireDay="+hireDay+"]";
}
//固定类名省去
public String toString(){
return getClass().getName()+"[name="+name+",salary="+salary+",hireDay="+hireDay+"]";
}
// 在超类调用getClass().getName()的情况下,子类定义如下
public String toString(){
return super.toString()+"[bonus="+bonus+"]";
}
- 在调用x.toString()的地方可用 “”+x 替代。好处是不用担心x是基本数据类型的情况
- System.out.println(x); // 若x是一对象,会直接调用x.toString()
- 针对数组而言,可使用Arrays.toString方法
附录- 相关API表
返回类型 | 方法名和参数 | 解释 |
---|---|---|
boolean | equals(Object obj) | 指示其他对象是否等于此 |
Class<?> | getClass() | 返回此 Object的运行时类,不是对象变量类型,是实际的类哦 |
int | hashCode() | 返回对象的散列码。散列码可以是任意整数,两个相等的对象应返回相等的散列码 |
String | toString | 返回对象的字符串表示形式:类名+散列值 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
static boolean | equals(Object a, Object b) | 如果参数相等返回 true,否则返回 false |
static int | hash(Object… objects) | 返回一个散列码,由提供的所有对象的散列码组合而得到 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
static boolean | equals(type[] a, type[] b) | 如果两个数组长度相同,且在对应为止上数据元素也相同,返回true |
static int | hashCode(type[] a) | 计算数组a的散列码 |
static String | toString(type[] a) | 返回指定数组的字符串形式 |
三、泛型数组列表–ArrayList
之前说过:数组一旦确定大小就不能再改变了,为更切合需求,ArrayList出现,其具有自动调节数组容量的功能。ArrayList是一个采用类型参数的泛型类。【泛型知识在第7章介绍】
1. 使用
//声明和构造一个保存Employee对象的数组列表
ArrayList<Employee> staff = new ArrayList<Employee>();
ArrayList<Employee> staff = new ArrayList<>(); //javaSE7 之后可省略右边类型参数
ArrayList staff = new ArrayList(); //称作原始类型,jdkSE5前,不推荐使用
// 若清楚或可估计出数组的大小,可在填充数组之前调用ensureCapacity方法或直接初始容量
staff.ensureCapatict(100);
ArrayList<Employee> staff = new ArrayList<>(100); //初始容量
//返回数组列表中元素数目
staff.size(); //相当于数组中a.length
// add方法添加元素
Employee harry =new Employee(...);
staff.add(harry ); //尾部添加新元素
staff.add(i,harry); //指定位置插入新元素,i <= staff.size();
//remove方法删除指定位置上的元素
staff.remove(i); //返回删除元素
//修改数组列表元素
staff.set(i,harry); // i:整形索引 harry:Employee对象;i <= staff.size();
//获取数组列表元素
staff.get(i); //返回Employee对象(没有泛型时,会返回Object对象)
//一旦确定数组列表大小不再变化,可调用trimToSize方法,固定存储区域大小,垃圾回收器会回收多余的空间
//在调用trimToSize方法后,再次添加新元素会移动存储块
staff.trimToSize();
//数组列表转换为数组
Employee[] em =new Employee[staff.size()];
staff.toArray(em); //数组em直接被赋值(方法参数的引用传递),但也会返回Employee数组
//可使用for each循环遍历数组列表
for(Employee e:staff){
do something with e;
}
- 数组列表管理着对象引用的一个内部数组。若调用add方法且内部数组已满,数组列表就将自动地创建一个更大地数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
- 区分数组列表的容量(new ArrayList<>(100))与数组的大小(new Employee[100]):为数组分配100个元素的存储空间,数组就有100个空位置可以使用;而容量为100个元素的数组列表只是拥有保存100个元素的潜力,甚至在初始化构造之后,不含有任何元素。这也是为什么添加新元素要使用add,而不能使用set。
- ArrayList 实施插入和删除元素操作(add和remove方法)时,指定位置之后的所有元素要往后/前移,效率低。若要频繁插入或删除,应使用链表存储数据。【链表知识在第8章介绍】
2. 类型化与原始数组列表的兼容性
泛型是JDK5才出来的特性,那么与之前代码的兼容性如何呢?
// 假设一个遗留类
public class EmployeeDB{
public void update(ArrayList list){....}
public ArrayList find(String query){...}
......
}
// 假设类型化的数组列表对象
ArrayList<Employee> staff = new ArrayList<>();
//可直接将类型化的数组列表传递给原始数组列表,不需要进行任何类型转换
employeeDB.update(staff); //ok
//将原始数组列表赋给类型化的数组列表,会报警告
ArrayList<Employee> result = employeeDB.find(query); //warning
ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query); //哪怕强制类型转换,依旧有warning
// 可以用@SuppressWarning("unchecked")标注,来标记此处可接受类型转换,就不报警告了
@SuppressWarning("unchecked")
ArrayList<Employee> result = (ArrayList<Employee>)employeeDB.find(query); //ok
附录–ArrayList API 表
返回类型 | 方法名和参数名 | 解释 |
---|---|---|
boolean | add(E e) | 在数组列表的尾部添加一元素。永远返回true |
void | add(int index, E obj) | 在指定位置插入元素,后面元素依次向后移 |
int | size() | 返回存储在数组列表中的当前元素的数量 |
void | ensureCapacity(int capacity) | 确保数组列表在不重新分配存储空间的情况下就能够保存给定数量的元素 |
void | trimToSize() | 将数组列表的存储容量消解到当前尺寸 |
void | set(int index , E obj) | 设置数组列表指定位置上的元素 |
E | get(int index) | 获得指定位置的元素值 |
E | remove(int index) | 删除指定位置上的元素,后面元素依次向前移 |
T[] | toArray(T[] a) | 转换为数组 |
四、 对象包装器与自动装箱,参数可变,枚举类
1. 对象包装器与自动装箱
有时,需要将基本数据类型转换为对象,Java中,所有的基本类型都有一个与之对应的类,这些类称作包装器。包装器类是不可变的,即一旦构造了包装器,就不可更改包装在其中的值。而且包装器类被final修饰,不可定义其子类。
当需要基本类型对象时,使用包装器。比如数组列表的类型参数不允许是基本类型(ArrayList<int> 错误),此时应用到包装器类(ArrayList<Integer>)
基本类型 | 包装器 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
Void |
自动装箱和拆箱
ArrayList<Integer> list = new ArrayList<>();
list.add(Integer.valueOf(3)); //使用静态方法valueOf,将int型值转换为Integer对象
list.add(3); // 简化:自动装箱
int n = list.get(i).intValue(); //使用静态方法intValue,将Integer对象转换为int型值
int n = list.get(i); //简化:自动拆箱
//在算术表达式中也能自动装箱和拆箱
Integer n = 3; //装箱
n++; //拆箱
- 自动装箱规范要求boolean、byte、char <= 127,介于-128 ~ 127 之间的short和int 被包装到固定的对象中。故 在一些范围数值内,两个包装器对象比较时使用
==
结果为true,但局限性比较大。所以包装器对象比较时应调用equals方法。 - 包装器引用可以为null
- 一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double
- 装箱和拆箱时编译器认可的,而不是虚拟机。在编译生成的字节码文件中,装箱和拆箱调用的方法会呈现。
- 之前说过:在方法传参时,基本数据类型不可改变其值,引用类型可以。那是不是可将参数定义为Integer,就能编写一个修改数值参数值的方法?不可以,因为包装器类对象是不可变的。
下面为Integer常用API,其他包装器类也有类似的方法(此处不再呈现)。
返回值 | 方法名和参数 | 解释 |
---|---|---|
int | intValue() | 将Integer对象的值转为int |
static String | toString(int i) | 以新String对象的形式返回给定数值i的十进制表示 |
static String | toString(int i,int radix) | 返回数值i的基于给定radix参数进制的表示 |
static int | parseInt(String s) | 返回字符串s表示的整型数值,十进制表示 |
static int | parseInt(String s,int radix) | 返回字符串s表示的整型数值,radix参数进制表示 |
static Integer | valueOf(String s) | 返回用s表示的整型数值进行初始化的一个新Integer对象,返回十进制整数 |
static Integer | valueOf(String s,int radix) | 返回用s表示的整型数值进行初始化的一个新Integer对象,返回radix参数进制的整数 |
2. 参数数量可变的方法
在格式化输出时,也用到过参数数量可变的实现
System.out.printf("%d",n);
System.out.printf("%d %s",n,"widgets");
//尽管上面方法是一个是两个参数,一个是三个参数,但实际上是一个方法,那是怎么定义的呢?
public class PrintStream{
public PrintStream printf(String fmt,Object... args){
return format(fmt,args);
}
}
其上在参数类型后加...
就是可变参数的具体使用。Object… args 表明该方法可接受任意数量的对象(除fmt参数之外)。
实际上Object… args 就等同于 Object[ ] 数组。
System.out.printf("%d %s",new Object[]{new Integer(n),"widgets"});
- 用户可自定义可变参数的方法,参数可指定为任意类型,甚至是基本类型(因装箱的存在)
- 可将已存在且最后一个参数是数组的方法重新定义为可变参数的方法,而不会破坏任何已经存在的代码
3. 枚举类
回顾:枚举类型
enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE}; //自定义枚举类型
Size s= Size.MEDIUM; //声明
实际上,这个声明定义的类型是一个类,它刚好有4个实例。在比较两个枚举类型的值时,直接使用==
即可。
可以在枚举类型中添加构造器,方法或域。鉴于简化的考虑,Enum类省略了一个类型参数。实际上应将枚举类型Size扩展为Enum<Size>。
public 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;
}
}
所有的枚举类型都是Enum类的子类,继承了许多方法。
Size.SMALL.toString(); // 返回字符串“SMALL”
Size s = Enum.valueOf(Size.class,"SMALL"); //将s设置为Size.SMALL
Size[] values = Size.values(); //将返回一个包含全部枚举值的数组
Size.MEDIUM.ordinal(); //返回枚举常量的位置,从0开始计数
返回类型 | 方法名和参数 | 解释 |
---|---|---|
static Enum | valueOf(Class enumClass,String name) | 返回指定名字、给定类的枚举常量 |
String | toString | 返回枚举常量名 |
int | ordinal | 返回枚举常量在enum声明中的位置,位置从0开始计数 |
int | compareTo(E other) | 如果枚举常量出现在Other之前,返回一个负值,相等返回0,之后,返回正值 |
五、反射
反射库提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。反射是一种功能强大且复杂的机制。使用它的主要人员是工具构造者,而不是应用程序员。可选择跳过这部分,等用到时再看。
反射机制可用来:
- 在运行时分析类的能力
- 在运行时查看对象
- 实现通用得到数组操作代码
- 利用Method对象
1. Class类
在程序运行期间,Java运行时系统始终为所有对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。保存这些信息的类是Class类。
// 获取Class对象,有三种方式,假设obj是任一对象变量
① Class c1 = obj.getClass();
② Class c2 = Class.forName("java.util.Random"); //传入类名,包名也是类名一部分
③ Class c3 = T.class; //T是任意的Java类型,包括基本数据类型
//Class对象常见方法
c1.getName(); //返回类名(是包含包名的哦)
int[].class.getName(); //返回[I,数组类型调用getName要注意,其返回结果可能不是所期望的
c1.newInstance(); //动态创建一个类的实例,调用默认的构造器
- 在程序启动时,包含main方法的类被加载,它会加载所需要的类,这些被加载的类又要加载它们所需要的类。这样对于一个大型应用程序来说,会消耗大量时间。可以这样处理:首先确保main方法的类没有显式引用其他的类,其次显示一个启动画面,最后调用Class.forName手工地加载其他的类。给用户进入系统很快的幻觉。
- Class类实际上是一个泛型类,例如:Employee.class 的类型实际是Class<Employee>。但在大多数实际问题中,可忽略类型参数。
- getName方法应用在数组类型时,会返回一个奇怪的名字。如上:int数组返回 [I
- 与反射机制相关类的大部分方法会抛出一个异常,需要进行异常处理。【异常知识在第六章】
- 虚拟机为每个类型管理一个Class对象,因此,可用
==
运算符实现两个类对象比较的操作
2. 分析类的能力–即查看类中方法,域和构造器
在java.lang.reflect包中有3个类:Field、Method和Constructor,分别用于描述类的域、方法和构造器。
Class类中的getFields()、getMethods() 和 getConstructors() 方法分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。getDeclaredFields()、getDeclaredMethods() 和 getDeclaredConstructors() 方法分别返回类中声明的全部域、方法和构造器,其中包括私有成员和受保护成员,但不包括超类成员。
Field,Method,Constructor类中 都有getName() 方法返回项目的名称,getModifiers() 方法返回整型数值,用于标识修饰符,可借助Modifier类中静态方法得知整型数值对应的修饰符。
Method,Constructor类有getParameterTypes() 方法返回方法或构造器的参数类型。
Field类有getType() 方法返回域的类型;Method类中有getReturnType()
方法返回方法的返回类型。
方法表可见本节附录…
3. 在运行时分析对象–即对域值进行设置访问,调用方法和构造器
访问/设置域
Employee harry = new Employee("Harry Hacker",3500,10,1,1989); //声明Employee对象
Class c1 = harry.getClass(); //获取Employee类的Class对象
Field f = c1.getDeclaredField("name"); //获取本类name域的Field对象
f.setAccessible(true); //抑制Java语言访问检查
Object v = f.get(harry); //获取指定对象中指定域的值
f.set(harry,"en~kun"); //设置指定对象中指定域的值
需要注意:Java安全机制只允许查看任意对象有哪些域,而不允许读取它们的值。name域是私有域,是不可直接访问的,所以上面代码若没有setAccessible(true),就会抛出IllegalAccessError异常。
Field类中除了针对对象的get方法,还有针对基本类型的getInt,getDouble等方法可供使用。
到此,根据上面知识,可延申出反射的两种用法,一种:通用的toString方法(之前的toString方法都是显式调用其域,依靠随类应变,而通用toString方法什么都不需要知道,直接调用即可);另一种:泛型数组拷贝代码(在数组拷贝知识中:Arrays.copyOf 方法底层实现就是用了反射,返回的Object类型,可直接强转为传入的对象类型)【这两种用法的代码此处不展现,请查看反射应用–通用的toString方法,泛型数组代码】
调用方法和构造器
调用方法的核心就是Method中的invoke方法:invoke(Object obj, Object… args) 。obj是隐式参数:指明对象或者null(静态方法),args是显式参数:实参。
invoke方法的参数和返回值是Object,这意味着必须进行多次的类型转换,这样做会使编译器错过检查代码的机会,而且通过反射获得方法指针要比直接调用方法慢。所以不要使用Method对象进行回调功能,应使用接口或者lambda表达式进行回调。【接口和lambda表达式知识第5章介绍】
Employee harry = new Employee("Harry Hacker",3500,10,1,1989); //声明Employee对象
// 根据方法签名获取方法对象
Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("setSalary",double.class);
//调用方法
m1.invoke(harry);
m2.invoke(harry,3000);
调用构造器和调用方法的步骤一致:通过Class类获取指定构造器对象,再利用构造器中newInstance方法进行调用。
附录–用到的API表
返回类型 | 方法名和参数 | 解释 |
---|---|---|
static Class | forname(String classname) | 返回描述类名为className的Class对象 |
Object | newInstance() | 返回该类的一个新实例,会默认调用无参构造器 |
Field[] | getFields() | 返回该类及其超类的公有域,若Class对象是数组或基本类型,返回长度为0的数组 |
Field | getField(String name) | 返回指定名称的公有域 |
Field[] | getDeclaredFields() | 返回该类的全部域,包括私有和受保护 |
Field | getDeclaredField(String name) | 返回类中指定名称的域 |
Method[] | getMethods() | 返回该类及其超类的公有方法 |
Method | getMethod(String name, Class<?>… parameterTypes) | 返回指定方法签名的方法 |
Method[] | getDeclaredMethods() | 返回该类的所有方法,包括私有和受保护 |
Constructor<?>[] | getConstructors() | 返回该类及其超类的公有构造器 |
Constructor | getConstructor(Class<?>… parameterTypes) | 返回拥有指定参数的构造器 |
Constructor<?>[] | getDeclaredConstructors() | 返回该类的所有构造器,包括私有和受保护 |
boolean | isPrimitive() | 该Class对象表示的类型是否是基本数据类型 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
void | setAccessible(boolean flag) | 为反射对象设置可访问标志,flag为true表明屏蔽Java语言的访问检查 |
boolean | isAccessible() | 返回该反射对象是否可访问 |
static void | setAccessible(AccessibleObject[] array, boolean flag) | 设置对象数组可访问标志的快捷方法 |
Class<?> | getComponentType() | 返回数组的类型 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
String | getName() | 返回域的变量名 |
int | getModifiers() | 返回域的修饰符 |
Class<?> | getType() | 返回域的类型 |
Object | get(Object obj) | 返回obj对象中用Field对象表示的域值 |
xxxx | getxxxx(Object obj) | 一系列针对基本数据类型的方法 |
void | set(Object obj, Object value) | 用一个新值设置obj对象中Field对象表示的域 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
String | getName() | 返回方法名 |
int | getModifiers() | 返回方法的修饰符 |
Class<?>[] | getParameterTypes() | 返回参数类型,包装在Class对象数组中 |
Class<?> | getReturnType() | 返回方法返回类型 |
Class<?>[] | getExceptionTypes() | 返回方法抛出的异常类型的Class对象数组 |
Object | invoke(Object obj, Object… args) | 调用指定对象的方法,传递给定参数,返回方法返回值 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
String | getName() | 返回构造器名(包名.类名) |
int | getModifiers() | 返回构造器的修饰符 |
Class<?>[] | getParameterTypes() | 返回参数类型,包装在Class对象数组中 |
Class<?>[] | getExceptionTypes() | 返回构造器抛出的异常类型的Class对象数组 |
T | newInstance(Object… initargs) | 使用指定参数构造新实例 |
返回类型 | 方法名和参数 | 解释 |
---|---|---|
String | toString(int mod) | 返回修饰符的字符串表示 |
static boolean | isAbstract(int modifiers) | 修饰符中包含abstarct修饰符,返回true |
static boolean | isxxxx(int modifiers) | xxxx 可以是:Final,Interface,Private,Native,Protected, Public,Static…这里不再赘述 |
六、继承的设计技巧
技巧 | 解释 |
---|---|
将公共操作和域放在超类 | 体现代码重用性 |
不要使用受保护的域 | 子类集合是无限制的,子类过多会破坏封装性; 在Java中,同一个包中的所有类都可以访问protected域,而不管其是否为这个类的子类 |
使用继承实现“is-关系” | 使用继承很容易达到节省代码的目的,但有时会被滥用,需要注意。 |
除非所有继承的方法都有意义,否则不要使用继承 | – |
在覆盖方法时,不要改变预期的行为 | 比如add方法就是添加元素,重写的方法不能偏离最初的设计想法 |
使用多态,而非类型信息 | 使用多态方法或接口编写的代码比使用多种类型进行检测的代码更加易于维护和扩展 |
不要过多的使用反射 | 对于反射代码,编译器很难发现程序中的错误,只有在运行时才可发现 |