《Java核心技术 卷Ⅰ》 第5章 继承
- 类、超类、子类
- Object:所有类的超类
- 泛型数组列表
- 对象包装器与自动装箱
- 参数数量可变的方法
- 枚举类
- 继承的设计技巧
类、超类和子类
定义子类
关键字extend
表示继承。
public class Manager extends Employee
{
// 添加方法和域
}
extend
表明正在构造的新类派生于一个已存在的类。
已存在的类称为超类(superclass)、基类(base class)或父类(parent class);
新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。
子类有超类没有的功能,子类封装了更多的数据,拥有更多的功能。
所以在扩展超类定义子类时,仅需要指出子类与超类的不同之处。
覆盖方法
有时候,超类的有些方法并不一定适用于子类,为此要提供一个新的方法来覆盖(override)超类中的这个方法:
public class Manager extends Employee
{
private double bonus;
...
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
...
}
这里由于Manager
类的getSalary
方法并不能直接访问超类的私有域。
这是因为尽管子类拥有超类的所有域,但是子类没法直接获取到超类的私有部分,因为超类的私有部分只有超类自己才能够访问。而子类想要获取到私有域的内容,只能通过超类共有的接口。
而这里Employee
的公有方法getSalary
正是这样的一个接口,并且在调用超类方法使,要使用super
关键字。
那你可能会好奇:
- 不加
super
关键字不行么? -
Employee
类的getSalary
方法不应该是被Manager
类所继承了么?
这里如果不使用super
关键字,那么在getSalary
方法中调用一个getSalary
方法,势必会引起无限次的调用自己。
关于super
和this
需要注意的是:他们并不类似,因为super
不是一个对象的引用,不能将super
赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
子类构造器
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
语句super(...)
是调用超类中含有对应参数的构造器。
Q1:为什么要这么做?
A1:由于子类的构造器不能访问超类的私有域,所以必须利用超类的构造器对这部分私有域进行初始化。但是注意,使用super
调用构造器的语句必须是子类构造器的第一条语句。
Q2:一定要使用么?
A2:如果子类的构造器没有显示地调用超类构造器,则将自动地调用超类默认(没有参数)的构造器;如果超类并没有不带参数构造器,并且子类构造器中也没有显示调用,则Java编译器将报告错误。
Employee[] staff = new Employee[3];
staff[0] = manager;
staff[1] = new Employee(...);
staff[2] = new Employee(...);
for(Employee e : staff)
{
System.out.println(e.getName() + "" + e.getSalary());
}
这里将e
声明为Emplyee
对象,但是实际上e
既可以引用Employee
对象,也可以引用Manager
对象。
- 当引用
Employee
对象时,e.getSalary()
调用的是Employee
的getSalary
方法 - 当引用
Manager
对象时,e.getSalary()
调用的是Manager
的getSalary
方法
虚拟机知道e
实际引用的对象类型,所以能够正确地调用相应的方法。
一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为自动绑定(dynamic binding)。
继承层次
集成并不仅限于一个层次。
由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy),在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。
Java不支持多继承,有关Java中多继承功能的实现方式,见下一章有关接口的部分。
多态
"is-a"
规则的另一种表述法是置换法则,它表明程序中出现超类对象的任何地方都可以用子类对象置换。
Employee e;
e = new Employee(...);
e = new Manager(...);
在Java程序设计语言中,对象变量是多态的。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
这个例子中,虽然staff[0]
与boss
引用同一个对象,但是编译器将staff[0]
看成Employee
对象,这意味着这样调用是没有问题的:
boss.setBonus(5000); // OK
但是不能这样调用:
staff[0].setBonus(5000); // Error
这里因为staff[0]
声明的类型是Employee
,而setBonus
不是Employee
类的方法。
尽管把子类赋值给超类引用变量是没有问题的,但这并不意味着反过来也可以:
Manager m = staff[2]; // Error
如果这样赋值成功了,那么编译器将m
看成是一个Manager
类,在调用setBonus
由于所引用的Employee
类并没有该方法,从而会发生运行时错误。
理解方法调用
弄清楚如何在对象上应用方法调用非常重要。
比如有一个C
类对象x
,C
有一个方法f(args)
。
现在以调用x.f(args)
为例,说明调用过程的详细描述:
- 编译器查看对象的声明类型和方法名。
C
类中可能有多个同名的方法,编译器将列举所有C
类中名为f
的方法和其超类中属性为public
且名为f
的方法(超类私有无法访问)。 - 接下来,编译器查看调用方法时提供的参数类型。如果存在一个完全匹配的
f
,就选择这个方法,这个过程被称为重载解析(overloading resoluton)。这个过程允许类型转换(int转double,Manager转Employee等等)。如果编译器没有找到,或者发现类型转换后,有多个方法与之匹配,就会报告一个错误。 - 如果是
private
方法,static
方法、final
方法或者构造器,编译器将可以准确知道应该调用哪个方法,这种称为静态绑定(static binding)。如果不是这些,那调用的方法依赖于隐式参数的实际类型,并且在运行时动态绑定,比如x.f(args)
这个例子。 - 当程序运行时,并且动态绑定调用时,虚拟机一定调用与
x
所引用对象的实际类型最合适的那个类的方法。比如x
实际是C
类,它是D
类的子类,如果C
类定义了方法f(String)
,就直接调用;否则将在C
类的超类中寻找,以此类推。简单说就是顺着继承层次从下到上的寻找方法。
如果每次调用方法都要深度/广度遍历搜索继承链,时间开销非常大。
因此虚拟机预先为每个类创建一个方法表(method table),其中列出了所有方法的签名和实际调用的方法,这样一来在真正调用时,只需要查表即可。
如果调用super.f(args)
,编译器将对隐式参数超类的方法表进行搜索。
之前的Employee
类和Manager
类的方法表:
Employee:
getName() -> Employee.getName()
getSalary() -> Employee.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
Manager:
getName() -> Employee.getName()
getSalary() -> Manager.getSalary()
getHireDay() -> Employee.getHireDay()
raiseSalary(double) -> Employee.raiseSalary(double)
setBonus(double) -> Manager.setBonus(double)
在运行时,调用e.getSalary()
的解析过程:
- 虚拟机提取实际类型的方法表(之所以叫实际类型,是因为
Employee e
可以引用所有Employee
类的子类,所以要确定实际引用的类型)。 - 虚拟机搜索定义
getSalary
签名的类,虚拟机确定调用哪个方法。 - 最后虚拟机调用方法。
注:在覆盖一个方法时,子类方法不能低于超类方法的可见性,特别是超类方法是public
,子类覆盖方法时一定声明为public
,因为经常会发生这样的错误:在声明子类方法时,因为遗漏public
而使编译器把它解释为更严格的访问权限。
阻止继承:final类和方法
有时候,可能希望阻止人们利用某个类定义子类。
不允许扩展的类被称为final
类。
如果在定义类时使用了final
修饰符就表明这个类是final
类。
public final class Executive extends Manager
{
...
}
方法也可以被声明为final
,这样子类就不能覆盖这个方法,final
类中的所有方法自动地称为final
方法。
public class Employee
{
...
public final String getName()
{
return name;
}
...
}
这里注意final
域的区别,final
域指的是构造对象后就不再运行改变他们的值了,不过如果一个类声明为final
,只有其中的方法自动地成为final
,而不包括域。
将方法或类声明为final
主要目的是:确保不会在子类中改变语义。
强制类型转换
有时候就像将浮点数转换为整数一样,也可能需要将某个类的对象引用转换成另一个类的对象引用。
对象引用的转换语法与数值表达式的类型转换类似,仅需要一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。
Manager boss = (Manager) staff[0];
// 因为之前把boss这个Manager类对象也存在了Employee数组中
// 现在通过强制类型转换回复成Manager类
进行类型转换的唯一原因:在暂时忽略对象的实际类型之后,使用对象的全部功能。
在Java中,每个对象变量都属于一个类型,类型描述了这个变量所引用的以及能够引用的对象类型。
将一个值存入变量时,编译器将检查是否允许该操作:
- 将一个子类的引用赋给一个超类变量,编译器时允许的
- 但是将一个超类引用赋给一个子类变量,必须进行类型转化,这样才能通过运行时的检查
如果试图在继承链上进行向下的类型转换,并谎报有关对象包含的内容(比如硬要把一个Employee
类对象转换成Manager
类对象):
Manager boss = (Manager) staff[1]; // Error
运行时,Java运行时系统将报告这个错误(不是在编译阶段),并产生一个ClassCastException
异常,如果没有捕获异常,程序将会终止。
所以应该养成一个良好习惯:在进行类型强转之前,先查看一下是否能成功转换,使用instanceof
操作符即可:
if(staff[1] instanceof Manager)
{
boss = (Manager) staff[1];
...
}
注:如果x
为null
,则它对任何一个类进行instanceof
返回值都是false
,它因为没有引用任何对象。
抽象类
位于上层的类通常更具有通用性,甚至可能更加抽象,对于祖先类,我们通常只把它作为派生其他类的基类,而不作为想使用的特定的实例类,比如Person
类对于Employee
和Student
类而言。
由于Person
对子类一无所知,但是又想规范他们,一种做法是提供一个方法,然后返回空的值,另一种就是使用abstract
关键字,这样Person
就完全不用实现这个方法了。
public abstract String getDescription();
// no implementation required
为了提供程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person
{
private String name;
...
public abstract String getDescription();
...
public String getName()
{
return name;
}
}
除了抽象方法外,抽象类还可以包含具体数据和具体方法。
尽管许多人认为,在抽象类中不能包含具体方法,但是还是建议尽量把通用的域和方法(不管是否抽象)都放在超类(不管是否抽象)中。
虽然你可以声明一个抽象类的引用变量,但是只能引用非抽象子类的对象,因为抽象类不能被实例化。
在非抽象子类中定义抽象类的方法:
public class Student extends Person
{
private String major;
...
public String getDescription()
{
return "a student majoring in " + major;
}
}
尽管Person
类中没有具体定义getDescription
的具体内容,但是当一个Person
类型引用变量p
使用p.getDescription()
也是没有问题的,因为根据前面的方法调用过程,在运行时,方法的实际寻找是从实际类型开始寻找的,而实际类型都是定义了这个方法的具体内容。
那你可能会问,我可以只在Student
类中定义getDescription
不就行了么?为什么还要在Person
去声明?因为如果这样的话,就不能通过p调用getDescription
方法了,因为编译器只允许调用在类中声明的方法。
受保护访问
有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类中的某个域,而不让其他类访问到。
为此,需要将这些方法或域声明为protected
。
例如,如果Employee
中的hireDay
声明为protected
,而不是private
,则Manager
中的方法就可以直接访问它。
不过,Manager
中的方法只能够访问Manager
对象中的hireDay
,而不能访问其他Employee
对象中的这个域,这样使得子类只能获得访问受保护域的权利。
对于受保护的域来说,但是这在一定程度上违背了OOP提倡的数据封装原则,因为如果当一个超类进行了一些修改,就必须通知所有使用这个类的程序员(而不像普通的private
域,只能通过开放的方法去访问)。
相比较,受保护的方法更具有实际意义。如果需要限制一个方法的使用,就可以声明为protected
,这表明子类得到信任,可以正确地使用这个方法,而其他类(非子类)不行。
这种方法的一个最好示例就是Object
类中的clone
方法。
归纳总结Java控制可见性的4个访问修饰符:
-
public
:对所有类可见 -
protected
:对本包和所有子类可见 -
private
:仅对本类可见 - 默认,无修饰符:仅对本包可见
Object:所有类的超类
Obejct
类是Java中所有类的始祖,Java中每个类都是它扩展而来。
如果没有明确指出超类,Object就被认为是这个类的超类。
自然地,可以使用Object
类型的变量引用任何类型的对象:
Obejct obj = new Employee("Harry Hacker", 35000);
Object
类型的变量只能用于各种值的通用持有者。如果想要对其中的内容进行具体操作,还需要清楚对象的原始类型,并进行相应的类型转换:
Employee e = (Employee) obj;
equals方法
Object
类中的equals
方法用于检测一个对象是否等于另外一个对象。
这里的等于指的是判断两个对象是否具有相同的引用。
但是在判断两个不确定是否为null
的对象是否相等时,需要使用Objects.equals
方法,如果两个都是null
,将返回true
;如果其中一个为null
,另一个不是,则返回false
;如果两个都不为null
,则调用a.equals(b)
。
当然大多数时候Object.equals
并不能满足,一般来说我们需要比较两个对象的状态是否相等,这个时候需要重写这个方法:
public class Manager extends Employee
{
...
public boolean equals(Object otherObject)
{
// 首先要调用超类的equals
if(!super.equals(otherObejct)) return false;
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
}
相等测试与继承
在阅读后面的书籍笔记内容之前,首先补充一下getClass
和instanceof
到底是什么:
- obejct.getClass():返回此
object
的运行时类Class
(Java中有一个类叫Class)。比如一个Person
变量p
,则p.getClass()
返回的就是Person
这个类的Class
对象,Class
类提供了很多方法来获取这个类的相关信息 - obejct instanceof ClassName:用来在运行时指出这个对象是否是这个特定类或者是它的子类的一个实例,比如
manager instanceof Employee
是返回true
的
好了,让我们回到原书吧。
如果隐式和显示的参数不属于同一个类,equals
方法如何处理呢?
有许多程序员喜欢使用instanceof
来进行检测:
if(!otherObject instanceof Employee) return false;
这样做不但没有解决otherObject
是子类的情况,并且还可能招致一些麻烦。
Java语言规范要求equals
方法具有下面的特性:
- 自反性:任何非空引用
x
,x.equals(x)
应返回true
- 对称性:任何引用
x
和y
,y.equals(x)
返回true
,则x.equals(y)
也应该返回true
- 传递性:任何引用
x
,y
和z
,如果x.equals(y)
返回true
,y.equals(z)
返回true
,则x.equals(z)
也应该返回true
- 一致性:如果
x
和y
引用对象没有发生变化,反复调用x.equals(y)
应该返回同样结果 - 任意非空引用
x
,x.equals(null)
应该返回false
从两个不同的情况看一下这个问题:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用
getClass
进行检测 - 如果由超类决定相等的概念,那么就可以使用
instanceof
进行检测,这样可以在不同子类的对象之间进行相等的比较
给出一个编写完美equals
方法的建议:
- 显示参数命名为
otherObejct
,稍后将它转换成另一个叫做other
的变量 -
检测
this
与otherObject
是否因用同一个对象:if(this == otherObject) return true;
-
检测
otherObject
是否为null
:if(otherObject == null) return false;
-
比较
this
和otherObject
是否属于同一个类// 如果equals语义在每个子类中有改变,就用getClass if(getClass() != otherObject.getClass()) return false; // 如果子类拥有统一的语义,就用instanceof检测 if(!(otherObejct instanceof ClassName)) return false;
-
将
otherObejct
转换为相应的类类型变量:ClassName other = (ClassName) otherObejct;
-
开始进行域的比较,使用
==
比较基本类型域,使用Objects.equals
比较对象域return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;
如果在子类中重新定义equals
,还要在其中包含调用super.equals(other)
。
另外,对于数组类型的域,可以使用静态的Array.equals
方法检测相应的数组元素是否相等。
hashCode方法
散列码(hash code)是由对象导出的一个整数值。
散列码是没有规律的,如果x
和y
是两个不同的对象,x.hashCode()
与y.hashCode()
基本上不会相同。
对于String
类而言,字符串的散列码是由内容导出的。
由于hashCode方法定义在Object
类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
如果重新定义equals
方法,就必须重新定义hashCode
方法,以便用户可以将对象插入到散列表中。
hashCode
方法应该返回一个整数数值(也可以是负数),并合理地组合实例域的散列码,以便让各个不同的对象产生的散列码更加均匀。
例如,Employee
类的hashCode
方法:
public class Employee
{
public int hashCode()
{
return 7 * name.hashCde()
+ 11 * new Double(salary).hashCode()
+ 13* hireDay.hashCode();
}
}
不过如果使用null
安全的方法Objects.hashCode(...)
就更好了,如果参数为null
,这个方法返回0。
另外,使用静态方法Double.hashCode(salary)
来避免创建Double
对象。
还有更好的做法,需要组合多个散列值时,可以调用Objects.hash
并提供多个参数。
public int hashCode()
{
return Obejcts.hash(name, salary, hireDay);
}
equals
与hashCode
的定义必须一致:如果x.equals(y)
返回true
,那么x.hashCode()
就必须与y.hashCode()
具有相同的值。
toString方法
Object
中还有一个重要的方法,就是toString
方法,它用于返回表示对象值的字符串。
绝大多数(但不是全部)的toString
方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。
public String toString()
{
return getClass().getName()
+ "[name=" + name
+ ",salary=" + salary
+ ",hireDay=" + hireDay
+ "]";
}
toString
方法也可以供子类调用。
当然,设计子类的程序员也应该定义自己的toString
方法,并将子类域的描述添加进去。
如果超类使用了getClass().getName()
,子类只需要调用super.toString()
即可。
public class Manager extends Employee
{
...
public String toString()
{
return super.toString()
+ "[bonus=" + bonus
+ "]";
}
}
现在,Manager
对象将打印输出如下所示内容:
Manager[name=...,salary=...,hireDay=...][bonus=...]
注意这里在子类中调用的super.toString()
,不是在超类Employee
中调用的么?为什么打印出来的是Manager
?
因为getClass
正如前面所说,获取的是这个对象运行时的类,与在哪个类中调用无关。
如果任何一个对象x
,调用System.out.println(x)
时,println
方法就会直接调用x.toString()
,并打印输出得到的字符串。
Object
类定义了toString
方法,用来打印输出对象所属类名和散列码:
System.out.println(System.out)
// 输出 java.io.PrintStream@2f6684
这样的结果是PrintStream
类设计者没有覆盖toString
方法。
对于一个数组而言,它继承了object
类的toString
方法,数组类型按照旧的格式打印:
int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
String s = "" + luckyNumbers;
// s [I@1a46e30
前缀[I
表明是一个整形数组,如果想要得到里面内容的字符串,应该使用Arrays.toString
:
String s = Arrays.toString(luckyNumbers);
// s [2,3,5,7,11,13]
如果想要打印多维数组,应该使用Arrays.deepToString
方法。
强烈建议为自定义的每一个类增加toString
方法。
泛型数组列表
在许多程序设计语言中,必须在编译时就确定整个数组大小。
在Java中,允许运行时确定数组的大小:
int actualSize = ...;
Employee[] staff = new Employee[actualSize];
当然,这段代码并没有完全解决运行时动态更改数组的问题。一旦确定了大小,想要改变就不容易了。
在Java中,最简单的解决方法是使用Java中另一个被称为ArrayList
的类,它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。
ArrayList
是一个采用类型参数(type paraneter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如ArrayList<Employee>
。
ArrayList<Employee> staff = new ArrayList<Employee>();
// 两边都是用参数有些繁琐,在Java SE 7中,可以省去右边的类型参数
ArrayList<Employee> staff = new ArrayList<>();
这一般叫做“菱形语法”(<>
),可以结合new
操作符使用。
如果赋值给一个变量,或传递到某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在<>
中。
在这个例子中,new ArrayList<>()
将赋值给一个类型为ArrayList<Employee>
的变量,所以泛型类型为Employee
。
使用add
方法可以将元素添加到数组列表中。
staff.add(new Employee(...));
数组列表管理着对象引用的一个内部数组,最终数组空间有可能被用尽,这时数组列表将会自动创建一个更大的数组,并将所有的对象从较小数组中拷贝到较大数组中。
也可以确定存储的元素数量,在填充数组前调用ensureCapacity
方法:
// 分配一个包含100个对象的内部数组
// 在100次调用add时不用再每次都重新分配空间
staff.ensureCapacity(100);
// 当然也可以通过把初始容量传递给构造器实现
ArrayList<Employee> staff = new ArrayList<>(100);
size方法返回数组列表包含的实际元素数目:
staff.size()
一旦能够确认数组列表大小不再发生变化,可以调用trimToSize
方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器将回收多余的存储空间。
访问数组列表元素
数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。
需要使用get
和set
方法实现或改变数组元素的操作,而不是[index]
语法格式。
staff.set(i, harry);
Employee e = staff.get(i);
当没有泛型类时,原始的ArrayList
类提供的get
方法别无选择只能返回Object
,因此,get
方法的调用者必须对返回值进行类型转换:
Employee e = (Employee) staff.get(i);
当然还是有一个比较方便的方法来灵活扩展又方便访问:
ArrayList<X> list = new ArrayList<>();
while(...)
{
x = ...;
list.add(x);
}
X[] a = new X[list.size()];
// 使用toArray方法把数组元素拷贝到一个数组中
list.toArray(a);
还可以在数组列表的中间插入元素:
int n = staff.size()/2;
staff.add(n, e);
当然也可以删除一个元素:
Employee e = staff.remove(n);
可以使用for each
循环遍历数组列表:
for(Employee e : staff)
do sth with e
对象包装器与自动装箱
有时需要将int
这样的基本类型转换为对象,所有基本类型都有一个与之对应的类。
例如,Integer
类对应基本类型int
,通常这些类称为包装器(wrapper)。
这些对象包装器有很明显的名字:Integer
、Long
、Float
、Double
、Short
、Byte
、Character
、Void
和Boolean
(前6个类派生于公共超类Number
)。
对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
同时,对象包装器类还是final
,因此不能定义它们的子类。
有一个很有用的特性,便于添加int
类型的元素到ArrayList<Integer>
中。
ArrayList<Integer> list = new ArrayList<>();
list.add(3);
// 这里将自动地变为
list.add(Integer.valueOf(3));
这种变换被称为自动装箱(autoboxing)。
相反地,将一个Integer
对象赋给一个int
值时,将会自动地拆箱。
int n = list.get(i);
// 将会被翻译成
int n = list.get(i).intValue();
在算术表达式中也能自动地装箱和拆箱,例如自增操作符应用于一个包装器引用:
Integer n = 3;
n++;
编译器自动地插入一条对象拆箱指令,然后自增,然后再结果装箱。
==
虽然也可以用于对象包装器对象,但一般检测是对象是否指向同一个存储区域。
Integer a = 1000;
Integer b = 1000;
if(a == b) ...;
然而Java中上面的判断是有可能(may)成立的(这也太玄学了),所以解决办法一般是使用equals
方法。
还有一些需要强调的:
- 包装器引用可以为
null
,所以自动装箱可能会抛出NullPointerException
异常 - 如果条件表达式中混用
Integer
和Double
类型,Integer
值就会拆箱,提升为double
,再装箱为Double
- 装箱和拆箱是编译器认可的,而不是虚拟机,编译器在生成类字节码时,插入必要的方法调用,虚拟机只是执行这些字节码(就相当于一个语法糖吧)。
使用数值对象包装器还有另外一个好处,可以将某些基本方法放置在包装器中,比如,将一个数字字符串转换成数值。
int x = Integer.parseInt(s);
参数数量可变的方法
Java SE 5以前的版本中,每个Java方法都有固定数量的参数,然而现在的版本提供了可变的参数数量调用的方法。
比如printf
方法的定义:
public class PrintStream
{
public PrintStream printf(String fmt, Object... args)
{
return format(fmt, args);
}
}
这里的省略号...
是Java代码的一部分,表明这个方法可以接收任意数量的对象(除fmt参数外)。
实际上,printf
方法接收两个参数,一个是格式字符串,另一个是Object[]
数组,其中保存着所有的参数。
编译器需要对printf
的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱:
System.out.printf("%d %s", new Object[]{ new Integer(n), "widgets" });
用户也可以自定义可变参数的方法,并将参数指定为任意类型,甚至基本类型。
// 找出最大值
public static double max(double... values)
{
double largest = Double.NEGATIVE_INFINITY;
for(double v : values) if(v > largest) largest = v;
return largest;
}
枚举类
public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
实际上这个声明定义的类型是一个类,它刚好有4个实例。
因此比较两个枚举类型值时,不需要调用equals
,直接使用==
就可以了。
如果需要的话,可以在枚举类型中添加一些构造器、方法和域,构造器只在构造枚举常量的时候被调用。
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
类的子类,他们集成了这个类的许多方法,最有用的一个是toString
,这个方法能返回枚举常量名,例如Size.SMALL.toString()
返回"SMALL"
。
toString
的逆方法是静态方法valueOf
。
Size s = Enum.valueOf(Size.class, "SMALL");
将s
设置成Size.SMALL
。
每个枚举类型都有一个静态的values
方法,返回一个包含全部枚举值的数组。
Sizep[] values = Size.values();
ordinal
方法返回enum
声明中枚举常量的位置,位置从0开始技术。
反射
反射是一种功能强大且复杂的机制,使用它的主要人员是工具构造者,而不是应用程序员。
所以这部分先跳过,将会在以后一个专题单独来说明。
继承的设计技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现
"is-a"
关系 - 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为,不要偏离最初的设计想法
- 使用多态,而非类型信息
- 不要过多地使用反射
Java继承总结
- 子类(定义、构造器、方法覆盖)
- 继承层次
- 多态
- 方法调用的过程细节
- final类和方法
- 强制类型转换
- 抽象类
-
protected
受保护访问 -
Object
所有类的超类 -
equals
方法 - 相等测试与继承
-
hashCode
方法 -
toString
方法 - 泛型数组列表
- 对象包装器与自动装箱
- 参数数量可变的方法
- 枚举类
- 继承设计技巧
个人静态博客:
- 气泡的前端日记: https://rheabubbles.github.io
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。