2-1-类和对象

上一级:系统整理java知识体系

类的定义及使用

类的定义

类是构造对象的模板和蓝图

  1. 由类构造对象的过程称为"创建类的实例(instance)"

  2. 封装(亦称为数据隐藏),就是将数据和行为组合到一个包(package)中,并对对象的使用者隐藏具体的实现方式。

封装是处理对象的一个重要概念。

  1. 对象中的数据称为实例字段(instance field)

  2. 操作数据的方法称为方法(method),C++中使用函数实现

  3. 状态(state)是当前对象的所有当前状态的集合

  4. 通过扩展一个类来建立另外一个新类的过程称为继承(inheritance),值得注意的是继承一般会是一个更特殊的类继承一个更一般的类,同时会增加一些新的方法来处理所需的需求.

对象

对象是包含对用户公开的特定功能部分和隐藏的实现部分的集合

  1. 每个对象都有三个主要特性

    • 对象的行为(behavior)

    • 对象的状态(state)

    • 对象的标识(identity)

对应到Java中,分别是

行为 -> 可调用的方法 methods
状态 -> 描述当前状况的信息,比如各种变量
标识 -> 对象(或称对象实例)的名称

要注意的的是对象的状态会随着时间而发生改变,这种改变不能是自发的,否则就破坏了封装性

类的使用

Java中,使用构造器(constructor,或称构造函数)来构造对象实例。

  1. 构造器可以构造并初始化对象

  2. 构造器名字与类名相同,使用时在构造器前加上new操作符,如下:

1
new Date();
  1. 构造器构造出对象后,可以用一个对象变量来引用他,便于以后继续使用
1
2
3
4
Date birthday = new Date();

//继续使用
System.out.println(birthday);
  • 对象变量(如birthday)不是对象,实际上若没有对其指定初始状态(即赋初值),其就没有引用任何对象,这时就不能对这个对象变量使用对象(Date)可使用的方法.

若要使用则必须先初始化,有两种初始化方法

1
2
3
4
deadline = new Date();

//让它引用一个已有对象
deadline = birthday;
  • 可以显式的将对象变量设置为null表示其目前没有引用任何对象
1
deadline = null;
  • 所有的Java对象都储存在堆上,当一个对象包含另外一个对象变量时,它只是包含了指向另外一个堆对象的指针。

静态工厂方法(factory method)

  • 封装的意义就在于程序员不用考虑其内部的实例字段和具体方法实现,只需要考虑类对外提供的方法。
  • 更改器方法和访问器方法
    • 更改器方法会修改对象的状态
    • 访问器方法只访问对象而不修改对象

LocalDate类和Date类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//java.time.LocalDate;

//构造一个表示当前日期的对象
static LocalDate now()
//构造一个表现给定日期的对象,如1997-12-7
static LocalDate of(int year,int month,int day)
//得到当前日期的年月日
int getYear()
int getMonthValue()
int getDayofMonth()

//得到当前日期是星期几,作为DayOfWeek的一个对象实例返回,
//调用getValue来得到1-7之间的一个数,表示这是星期几
//1表示星期1,7表现星期日
DayOfWeek getDayOfWeek
//eg.
if(date.getDayOfWeek().getValue == 1){ System.out.println("今天星期1")}

//生成当前日期之后或者之前n天的日期
LocalDate plusDays(int n)//之后n天
LocalDate minusDays(int n)//之前n天

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.time.*;

/**
* @version 1.5 2015-05-08
* @author Cay Horstmann
* 这个示例展示了然后使用一个类的接口来完成相当复杂的任务如日历表,而不需要考虑具体实现,只需要使用import导入
*/
public class CalendarTest
{
public static void main(String[] args)
{
LocalDate date = LocalDate.now();
int month = date.getMonthValue();
int today = date.getDayOfMonth();

//假如今天4号,today=4,
// 4-1=3,
// date.minusDays(3)意思是今天之前3天的日期,即1号
date = date.minusDays(today - 1); // set to start of month

//得到这个月的第一天是星期几
//今天4号,对应是星期日,
// 这个月的第一天是星期4,即value=4
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue(); // 1 = Monday, . . . , 7 = Sunday

//打印日历中第一行的缩进,和表头
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for (int i = 1; i < value; i++)
System.out.print(" ");

//循环条件内设置,只打印当前月份的,到了下个月则循环结束
//因为我们不知道这个月有几天,那就设置date还在当前月份就继续迭代打印
while (date.getMonthValue() == month)
{
System.out.printf("%3d", date.getDayOfMonth());
//如果打印到当前日期 如4号,则用一个*标记
if (date.getDayOfMonth() == today)
System.out.print("*");
else
System.out.print(" ");

//日期加一天,并且当加到了下一个星期1时,自动换行
date = date.plusDays(1);
if (date.getDayOfWeek().getValue() == 1) System.out.println();
}
//循环体结束时,date应该是11月1日,星期日,所以要打印一个换行符结束
//如果正好这天是星期1,之前的循环体最后已经打印过一个换行符,则不需要再次打印一个
if (date.getDayOfWeek().getValue() != 1) System.out.println();
}
}

多个源文件

  • 可以任务Java的编译器内置了Unix或者Windows的make功能
  • 多个源文件编译有两种命令格式.

其中第二钟方式没有显式地编译Employee.java,但当Java编译器发现EmployeeTest.java使用了Employee类时,他会自动查找Employee.class文件,

如果没找到,就会寻找Employee.java并对它进行编译

javac Employee*.java
//或者
javac EmployeeTest.java

构造器

  • Java中的构造器总是要和new操作符一起使用,因为所有的Java对象都是在堆上构造的。初学者最容易犯的错误是忘记new操作符

var,声明局部变量

  • Java10中薪增了一个var关键字,用于声明局部变量,如果可以从变量的初始值直接推导出他们的类型,则无须在等号左侧指定类型,这种情况下可以使用var关键字
  • 要注意的是对应基本数值类型,如int,long,double。最好不要滥用var关键字,建议少用或者不用,以此帮你你培养对数值精度的敏感性。
1
2
3
4
5
//例如
Employee harry = new Employee("Harry Hacker",50000,1989,10,1);
//可以写成以下代码
var harry = new Employee("Harry Hacker",50000,1989,10,1);

使用null 引用

类的访问权限

私有方法

final实例字段

静态属性和方法

静态字段

如果将一个字段定义为static,这个字段就成为这个类所有对象共享访问的唯一标识码.

静态字段也被成为类字段,“静态”是沿用了C++的叫法,无实际意义。

静态常量

静态常量经常使用,如Math类中定义了一个静态常量PI

1
2
3
4
5
public class Math{
...
public static final double PI = 3.14;
...
}

在程序中访问静态常量可以通过Math.PI访问

类似的System.out中也有静态常量即 System.out.PrintStream

原生方法

即不是在Java语言中实现的方法,可以绕过Java的访问控制机制,实现修改final变量的值

静态方法

静态方法即不在对象上执行的方法,一般通过类名.方法的方式调用

1
Math.pow(x,a)
  • 也可以使用对象来调用静态方法(例如用harr.getNextId()代替Employee.getNextId()),这是合法的。但这种写法容易造成阅读上的语义混淆,不建议这样写

下面两种情况下可以使用静态方法:

  • 方法不需要访问对象状态,因为这个方法所需要的所有参赛都显式的通过传入的参数提供,如Math.pow()
  • 方法只需要访问类的静态字段,如Employee.getNextId()

工厂方法

静态方法的另外一种常见用途就是静态工厂方法(factory method)

1
2
LocalDate.now()
LocalDate.of()

下面给出常见的工厂方法的调用

1
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();

按值调用

Java总是按值调用的,即方法得到的所有参数值的一个副本,方法不能修改传递给他的任何参赛变量的内容,只是新建了一个副本,在这个副本基础上修改。方法结束时会调用gc,对副本进行销毁

  • 但把对象引用作为参数时就不同了,由于传递到方法中的对象引用和原来的对象引用实际上是在指向同一个堆上的对象,所以就可以达到修改这个堆上对象的作用

方法参数总结

  • 方法不能修改基本数据类型的参数(即数值型,布尔型等)
  • 方法可以改变对象参数的状态。(通过对象引用)
  • 方法不能让一个对象参数引用一个新的对象,(他们在方法内实现这些操作,但最后改变的只是对象引用的副本,并没有把改变的结果传递到方法外)

例如形参x、y交换了,实参a、b没有受到影响

src/v1ch04/ParamTest/ParamTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* This program demonstrates parameter passing in Java.
*
* @author Cay Horstmann
* @version 1.01 2018-04-10
*/
public class ParamTest {
public static void main(String[] args) {
/*
* Test 1: Methods can't modify numeric parameters
*/
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before: percent=" + percent);
tripleValue(percent);
System.out.println("After: percent=" + percent);

/*
* Test 2: Methods can change the state of object parameters
*/
System.out.println("\nTesting tripleSalary:");
var harry = new Employee("Harry", 50000);
System.out.println("Before: salary=" + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary=" + harry.getSalary());

/*
* Test 3: Methods can't attach new objects to object parameters
*/
System.out.println("\nTesting swap:");
var a = new Employee("Alice", 70000);
var b = new Employee("Bob", 60000);
System.out.println("Before: a=" + a.getName());
System.out.println("Before: b=" + b.getName());
swap(a, b);
System.out.println("After: a=" + a.getName());
System.out.println("After: b=" + b.getName());
}

public static void tripleValue(double x) // doesn't work
{
x = 3 * x;
System.out.println("End of method: x=" + x);
}

public static void tripleSalary(Employee x) // works
{
x.raiseSalary(200);
System.out.println("End of method: salary=" + x.getSalary());
}

public static void swap(Employee x, Employee y) {
Employee temp = x;
x = y;
y = temp;
System.out.println("End of method: x=" + x.getName());
System.out.println("End of method: y=" + y.getName());
}
}

class Employee // simplified Employee class
{
private final String name;
private double salary;

public Employee(String n, double s) {
name = n;
salary = s;
}

public String getName() {
return name;
}

public double getSalary() {
return salary;
}

public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}

对象构造

重载

显式字段初始化

Java中提供了下面的写法对类中的实例字段进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
class Empoyee{
//注意这里
private static int nextId;
private final int id = assignId();

//...
private static int assignId(){
int r = nextId;
nextId++;
return r;
}
}
  • C中不能直接初始化类的实例字段,所有的字段都必须放到构造器中设置,但C也提供了一种特殊语法,称作"初始化器列表语法"来达成对类的实例字段进行初始化的目的。

    但Java中不需要这么麻烦,因为Java中的对象没有字对象,只有指向其他对象的指针,C++这么做是因为其类中可以有子对象,所以用这样的初始化方法来防止错误的语法,即对子对象进行初始化

finalize

  • 不要使用finalize方法来完成清理,这个方法已经被废弃。
  • 可以改用close方法或者Runtime.addShutdownHook等方法。
  • Java9以后可以使用Cleaner类来注册一个动作。

继承

  • Java中,所有的继承都是公共继承(public),没有C++中的保护和私有继承

  • 超类和子类,分别对应其他程序设计语言中的双亲类和孩子类

    超super和子sub来自于计算机科学与数学理论中的集合语言的术语。

    所有员工组成的集合包含所有经理组成的集合。

即员工集合是经理集合的超集,反过来说,经理集合是员工集合的子集

  • 设计类的时候,应当将最一般的方法放在超类中,而将更特殊的方法放到子类

子类构造器

  • 子类构造器中调用超类的构造器,这条语句一定要写在子类构造器中的第一行
1
2
3
4
5
pubulic Manager(String name,double salary,int year,int month,int day){
//注意
super(name,salary,year,month,day);
bonus = 0;
}

类似与C++中在初始化列表中对超类进行构造

1
2
3
4
5
6
//C++
Manager::Manger(String name,double salary,int year,int month,int day)
:Employee(name,salary,year,month,day)
{
bonus = 0;
}

多态、动态绑定、虚拟(virtual)、final

对于下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// construct a Manager object
var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);

var staff = new Employee[3];

// fill the staff array with Manager and Employee objects

staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);

// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());

多态的含义

这里的e虽然声明为Employee类型,但当e引用staff[1]/staff[2]时,它可以引用Employee类型的对象,当e引用staff[0]时,也可以引用Manger类型的对象。

  • 一个对象变量(如e)可以指示多种实际类型的现象称为多态
  • 在运行时可以自动地选择适当的方法,称为动态绑定
  • C++中,要实现动态绑定,需要将成员函数(方法)声明为virtual
  • 而在Java中默认是动态绑定的。如果不想让一个方法动态绑定(即虚拟的),则要单独把他标记为final
1
2
3
4
//这种调用是可以的
boss.setBonus(5000);
//而这种调用是不行的
staff[0].setBonus(5000);

这个例子中,虽然staff[0]和boss引用同一个对象,但上面那种调用可以,下面那种调用却不行。

这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。

boss声明的类型是Manger。

i/1360519b.png

注意对象引用对象的区别。staff[0]已经有了指向,我们说是对象,而e是对象引用,e既可以指向Employee,也可以指向Manager.

i/1fa49b97.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
Test 子类引用的数组转换成超类引用的数组
Java中子类数组的引用可以转换为超类数组的引用,而不需要采用强制类型转换。
但是managers和staff1引用的是同一个堆上的对象
即如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常
具体如下
*/

//创建一个引用managers,它指向堆上的对象数组Manager[]
Manager[] managers = new Manager[10];

//让另外一个对象引用staff1来指向managers,
//这样Java中子类数组的引用可以转换为超类数组的引用
Employee[] staff1 = managers;//OK

//测试赋值操作,注意到编译器自动阻止了这个操作,并报出ArrayStoreException异常
//即如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常
staff1[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
我们再尝试分别对managers[0]和staff1[0]分别调用setBonus方法
注意到managers[0].setBonus(1000);可行
而staff1[0].setBonus(1000)不可行;
因为实际上managers和staff1引用的是同一个堆上的对象

*/

Manager[] managers = new Manager[10];
Employee[] staff1 = managers;//OK

managers[0].setBonus(1000);
System.out.println("name=" + managers[0].getName() );

staff1[0].setBonus(1000);
System.out.println("name=" + staff1[0].getName() );

不能将超类的引用赋给子类,例如,下面的赋值是非法的

1
Manager m = staff[i];

因为不是所有的员工都是经理,如果赋值成功,m就有可能引用了一个不是经理的Employee对象,这样m.setBonus就会报错。

多重继承

多重继承是一个子类可以有多个超类

Java不支持多重继承,但是提供了类似多重继承的功能,详情查看接口

  • C++中,一个类可以有多个超类

可见性

子类方法不能低于超类方法的可见性

即当超类是public时,子类不能是public,可以是public

经常出现的错误是忘记了子类的public修饰符,导致编译器报错

提示你试图提供更加严格的访问权限(private)

final类

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

  • 如你希望阻止人们派生出Executive的子类,就可以在声明Executive类时使用final修饰符。
1
2
3
public final class Executive extends Manager{
...
}
  • 将类中某个特定方法声明为final,这样子类就不能覆盖这个方法(final类中的所有方法自动称为final方法)
1
2
3
4
5
6
7
8
...
public class Employee{
...
public final String getName(){
return name;
}
...
}

强制类型转换

子类转换到超类是允许的,前面已经提到了。

但超类转换成子类却要受到需要限制

在Java中,试图进行从超类到子类的转换时需要使用强制类型转换,形如

1
Manager boss = (Manager) staff[0];

在运行时,Java的运行时系统会注意到这里,检查你的承诺是否符合,如果不符,将会抛出一个ClassCastException异常。

糟糕的是,如果没有捕获到这个异常,程序会自动的终止。

为此,我们应当使用instanceof操作符来检查,是否可以实现从超类到子类的强制类型转换(即检查你的承诺是否符合)

形如

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

这样,编译器会检查你的这个转换是否能成功,如果不可能成功就不会让你完成这个转换.例如下面的这个强制类型转换

1
String c = (String)staff[1];

明显的String不是Employee的子类,这个转换不可能成功,所以会编译错误

综上:

  • 只能在继承层次内进行强制类型转换,如
1
Manager boss = (Manager) staff[0];
  • 在超类转换成子类之前要使用instanceof运算符来进行检查

此外如果x为null,

x instanceof C ,

并不会产生异常,而是返回false。

这是因为null没有引用任何对象,自然也不会引用C类型的对象

  • 强制类型转换并不是一种好的做法,一般只用在当必须在超类对象上调用子类特有的方法时。
  • Java中必须要将instanceof运算符与强制类型转换结合起来使用

受保护访问protected

保护字段

  • Java中,保护字段只能由同一个包中的类访问。这是一个限制机制,防止通过派生子类来访问受保护的字段.

    假设包B只有子类Administrator,它是Employee的子类。同时Employee的其他子类都在包A中,这样Administrator类中的方法不能查看包A中其他的Employee对象的hireDay字段

保护方法

保护方法是更有实际意义的做法

如果需要限制某个方法的使用,即某个超类只信任他的子类,只希望他子类来正确地使用这个方法,而(不是他的子类的)其他的类不能正确使用这个方法,就可以把这个方法声明为protected

有一个很好的示例是Object类中的clone方法,这个方法是protected的

事实上,Java中的受保护及对所有的子类(无论在不在同一个包),及同一个包中所有其他的类(无论是不是子类)都可见

简化一下说法就是,Java中的protected对同一个包中所有类+另外一个包中的子类都可见,他们都可以访问。

Java的4个访问控制修饰符

1.仅对本类可见–private

2.对外部完全可见–public

3.对本包和所有子类可见–protected

4.对本包可见–默认,不需要修饰符

i/bfbfb04a.png

Java中的继承extends只能是public继承,不存在C++中的protected继承和private继承

同一个包下所有的子类和非子类

在 《Thinking in Java》 中,protected 的名称是「继承访问权限」,这也就是我们记忆中的 protected:protected 必须要有继承关系才能够访问。 所以你以为你懂了, 可是你真的理解了这句话吗?

先思考几个问题:

同一个包中, 子类对象能访问父类的 protected 方法吗?

不同包下, 在子类中创建该子类对象能访问父类的 protected 方法吗?

不同包下, 在子类中创建父类对象能访问父类的 protected 方法吗?

不同包下, 在子类中创建另一个子类的对象能访问公共父类的 protected 方法吗?

父类 protected 方法加上 static 修饰符又会如何呢?

《Thinking in Java》中有一句话:「protected 也提供包访问权限, 也就是说,相同包内的其他类可以访问 protected元素」, 其实就是 protected 修饰符包含了 default 默认修饰符的权限, 所以第 1 个问题你已经知道答案了, 在同一个包中, 普通类或者子类都可以访问基类的 protected 方法。

作者:云大数据社区

链接:https://juejin.im/post/6844903517988061191

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

父类为非静态(no static) protected 修饰类

1
2
3
4
5
6
7
8
9
10
package com.protectedaccess.parentpackage;

public class Parent {

protected String protect = "protect field";

protected void getMessage(){
System.out.println("i am parent");
}
}

不同包下子类访问父类的protected方法

若子类与基类不在同一包中,那么在子类中,子类实例访问的实际上是其从基类继承而来的protected方法,

子类中实际上把父类的方法继承下来了, 可以通过该子类对象访问, 也可以在子类方法中直接访问, 还可以通过 super 关键字调用父类中的该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.protectedaccess.parentpackage.sonpackage1;

import com.protectedaccess.parentpackage.Parent;

public class Son1 extends Parent{
public static void main(String[] args) {
Son1 son1 = new Son1();
son1.getMessage(); // 输出:i am parent,
}
private void message(){
getMessage(); // 如果子类重写了该方法, 则输出重写方法中的内容
super.getMessage(); // 输出父类该方法中的内容
}
}

不同包下,在子类中通过[父类引用]不可以访问父类 protected 方法

不同包下,在子类中不可以,通过[父类引用]访问其 protected 方法

无论是创建 Parent 对象还是通过多态创建 Son1 对象, 只要是父类引用( Parent 引用), 则不可访问, 编译器会提示错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.protectedaccess.parentpackage.sonpackage1;

import com.protectedaccess.parentpackage.Parent;

public class Son1 extends Parent{
public static void main(String[] args) {
Parent parent1 = new Parent();
// parent1.getMessage(); 错误

Parent parent2 = new Son1();
// parent2.getMessage(); 错误
}
}

不同包下,在子类中不能通过[另一个子类引用]访问共同基类的 protected 方法

1
2
3
4
5
6
7
package com.protectedaccess.parentpackage.sonpackage2;

import com.protectedaccess.parentpackage.Parent;

public class Son2 extends Parent {

}

//注意 Son2 是另一个子类, 在 Son1 中创建 Son2 的对象是无法访问他们共同的父类的 protected 方法的

1
2
3
4
5
6
7
8
9
10
package com.protectedaccess.parentpackage.sonpackage1;

import com.protectedaccess.parentpackage.Parent;
import com.protectedaccess.parentpackage.sonpackage2.Son2;
public class Son1 extends Parent{
public static void main(String[] args) {
Son2 son2 = new Son2();
// son2.getMessage(); 错误
}
}

父类为静态 protected 修饰类

对于protected的静态变量, 在子类中可以直接访问, 在不同包的非子类中则不可访问

1
2
3
4
5
6
7
8
9
10
package com.protectedaccess.parentpackage;

public class Parent {

protected String protect = "protect field";

protected static void getMessage(){
System.out.println("i am parent");
}
}

静态方法直接通过类名访问

无论是否同一个包,在子类中均可直接访问

1
2
3
4
5
6
7
8
9
package com.protectedaccess.parentpackage.sonpackage1;

import com.protectedaccess.parentpackage.Parent;

public class Son3 extends Parent{
public static void main(String[] args) {
Parent.getMessage(); // 输出: i am parent
}
}

在不同包下,非子类不可访问

1
2
3
4
5
6
7
8
9
package com.protectedaccess.parentpackage.sonpackage1;

import com.protectedaccess.parentpackage.Parent;

public class Son4{
public static void main(String[] args) {
// Parent.getMessage(); 错误
}
}

看到这里你应该知道有多少种情况了, 针对不同的情况都可能出现意外的结果, 所以还是得多实践, 仅仅在书上看一遍 protected 修饰符的作用是无法真正发现它的微妙。

继承的设计技巧

  • 将公共操作和字段放在超类中

例如把name放入Person类,而不是重复放在Employee和Student类中

  • 不要使用受保护的字段.
    protected机制并不能带了更多的保护,这是因为
    • 子类集合是无限制的,任何人都能在你的类的基础上派生出一个子类,然后通过这个子类访问protected字段
    • Java中,同一个包中所有的类都能访问protected实例字段,而不管它们是否为这个类的子类
  • 使用继承实现“is-a”关系

钟点工与员工之间不属于"is-a"关系,既钟点工不是特殊的员工。使用员工派生出钟点工不是一个好主意。这是因为钟点工会继承员工类的工资字段,而它自己又定义了一个时薪字段,这会带来无尽的麻烦,当你想要打印一份薪水单或者工资单时。

  • 除非所有继承来的方法都是有意义的,否则不要使用基础
  • 在覆盖方法时,不要改变预期的行为

例如某个方法预期要对day字段+1,那么在他的子类中你最好不要改变中国方法,把+1变成了-1,这会给你自己带来后续的迷惑。

  • 使用多态,而不用使用类型信息

例如

if(x is of type 1)
    action(x);
else if(x is of tyoe 2)
    action(x);

这样的情况更改考虑使用多态,把action方法放到两个type的超类或者接口中,然后调用x.action()解决问题。

即使用多态固有的动态分派机制执行正确的操作。

使用多态方法或接口实现的代码比使用多个类型检查的代码更加易于维护或者扩展。

  • 不要滥用反射

如果使用反射,编译器将无法帮助你找到编程错误,因为这些错误都发生在运行时,编译器帮不了你太多。只有在运行时才会发现错误并导致异常。

Object类

实际使用Object时,需要使用强制类型转换,转换为具体的对应的类型的对象,才能使用相对应类型的方法

包管理

包名

使用包名的主要原因是为了确保类名的唯一性,如果有两个相同的Employee类,将其放在不同的包中,就不会产生冲突。

为了保证包名的唯一性,采用将一个域名逆序的形式书写。如果com.horstmann.corejava

在编译器看来,嵌套的包之间没有任何关系,例如java.util包与java.util.jar包毫无关系。每个包都是一个独立的类集合。

类的导入

一个类可以使用所属包的所有类,以及其他包中的公共类。

有两种方式来访问另外一个包中的公共类。

第一中方式是使用完全限定名,就是包名后面跟着类名

1
java.time.LocalDate today = java.time.LocalDate.now();

另外一种方式是使用import语句,使用import语句后,在使用类名时,就不必写出类的全名了

1
2
import java.time.*;
LocalDate today = LocalDate.now();

即无需在前面加上包的前缀

还可以导入一个包中特定的一个类

1
import java.time.LocalDate;
1
注意,只能使用*导入一个包,不能使用形如import java.* 或者 import java.* 等语句一次导入多个包。

如果要编译器无法确定你想使用的是哪个Date类(有多个Date类),可以另起一行,新写一个特定的import语句

1
2
3
import java.util.*;
import java.sql.*;
import java.util.Date;

如果两个Date类都要使用,那么就要在每个Date类前加上完整的包名

1
2
var deadline = new java.util.Date();
var today = new java.sql.Date();
  • 类中的字节码总是使用完整的包名来引用其他包的类
  • 可以任务Java中的package和import语句相当于C++中的namespace 和using指令。

静态导入

使用下列这种import语句允许导入静态方法和静态字段而不只是类

1
import static java.lang.System.*;

这样就可以使用System中的静态方法和静态字段而不用加类名前缀

1
2
out.println("HelloWorld!");//i.e.,System.out
exit(0);//i.e.,System.exit

也可以导入特定的静态方法和静态字段

1
import static java.lang.System.out;

枚举

定义枚举类型

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

实际上这个声明定义的类型是一个类,他刚好有四个实例,且不可能构造新的对象了

比较乐观枚举类型的值时,不需要调用equals,直接使用"=="就可以了

此外,在需要的时候,也可以给枚举类型增加构造器,方法和字段

构造器只是在构造枚举常量的时候调用

1
2
3
4
5
6
7
8
9
enum Size
{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

Size(String abbreviation) { this.abbreviation = abbreviation; }
public String getAbbreviation() { return abbreviation; }

private final String abbreviation;
}

枚举的构造器总是私有的,所有可以省略private修饰符。

所有的枚举类型都是Enum类的,继承了这个类中的许多方法

toString方法

例如Size.SMALL.toString()将返回字符串"SMALL"

valueOf方法

与toString相对的逆方法是valueOf,例如

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

将s设置成Size.SMALL

此外,枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组

1
Size[] values = Size.values();

将返回包含元素Size.SMALL,Size.MEDIUM,Size.LARGE和Size.EXTRA_LARGE的数组

ordinal方法

返回enum声明中枚举常量的位置。位置从0开始计数,例如Size.MEDIUM。ordinal()返回1.

下面是EnumTest的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package enums;

import java.util.*;

/**
* This program demonstrates enumerated types.
* @version 1.0 2004-05-24
* @author Cay Horstmann
*/
public class EnumTest
{
public static void main(String[] args)
{
var 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");

Size(String abbreviation) { this.abbreviation = abbreviation; }
public String getAbbreviation() { return abbreviation; }

private final String abbreviation;
}

反射

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

  • 在运行时分析类的能力

  • 在运行时检查对象,例如,编写一个适用于所有类的toString方法

  • 实现泛型数组操作代码

  • 利用Method对象,这个对象很像C++中的函数指针。

  • 下一节: java接口与抽象类