静态工厂方法

类似LocalDate和NumberFormat的类使用静态工厂方法来构造对象。

1
2
3
4
5
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.gatPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); //0.1
System.out.println(percentFormatter.format(x)); //10%

这里的NumberFormat类不使用构造器来完成,有两个原因:

  • 无法命名构造器。构造器名字必须与类相同,这里希望有两个不同名字,分别得到货币实例和百分比实例。
  • 使用构造器时,无法改变所构造对象的类型,而工厂方法实际上将返回DecimalFormat类的对象,是NumberFormat的子类

Main方法

main方法也是一种静态方法。main方法不对任何对象进行操作,事实上,启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。

方法参数

按值调用——表示方法接受的是调用者提供的值;

按引用调用——表示方法接收的是调用者提供的变量地址。

Java总是按值调用的。方法得到的是所有参数值的一个副本。也就是说,方法不能修改传递给它的任何参数变量的内容。

1
2
double percent = 10;
harry.raiseSalary(percent);

无论方法如何实现,在这个方法调用后,percent值还是10。

但是对于对象引用则不同!

1
2
3
4
public static void tripleSalary(Employee x)
{
x.raiseSalary(200);
}

当调用

1
2
3
harry = new Employee(...);

tripleSalary(harry);

具体为:

  1. x初始化为harry值的一个副本,这里就是一个对象引用。
  2. raiseSalary方法应用于这个对象引用。x和salary同时引用的那个Employee对象的工资提高了200%。
  3. 方法结束后x不再使用,对象变量harry继续引用那个工资增至3倍的员工对象

总结Java方法参数

  • 方法不能修改基本数据类型的参数
  • 方法可以改变对象参数的状态
  • 方法参数不能让一个对象参数引用一个新的对象

对象构造

重载——同方法、不同参数

默认字段初始化:如果构造器中没有显示地为字段设置初值,则会被自动的赋为默认值!数值为0、布尔值为false、对象引用为null

无参构造器:如果编写一个类没有无参构造,就会为你提供一个无参数的构造器,如果已经只定义了有参,再调无参则不合法。

参数名的定义:

习惯将参数名和实例字段保持一致,通过this来区分:

1
2
3
4
5
public Employee(String name,double salary)
{
this.name = name;
this.salary = salary
}

this的另一用法:

this除了可以指示一个方法的隐式参数外,还可以调用同一个类的另一个构造器

1
2
3
4
5
public Employee(double s)
{
this("Employee #" + nextId,s);
nextId ++;
}

当调用new Employee(6000),Employee(double)构造器会调用Employee(String,double)构造器。

初始化块:

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
class Employee 
{
private static int nextId;
private int id;
private String name;
private double salary;

//初始化块
{
id = nextId;
nextId ++;
}

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

之前有两种初始化数据字段的方法:

  • 构造器中赋值
  • 声明中赋值

另一个则是设置一个初始化块,只要构造这个类的对象,初始化块就会被执行——首先运行初始化快,然后才运行构造器的主体部分。

但是这不是必需的,通常将初始化代码放在构造器中

区分于静态字段对应的静态代码块:如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块

区分初始化块和静态初始化块:

  • 静态初始化块:使用static定义,当类装载到系统时执行一次.若在静态初始化块中想初始化变量,那仅能初始化类变量,即static修饰的数据成员.
  • 非静态初始化块:在每个对象生成时都会被执行一次,可以初始化类的实例变量.

类设计技巧

  • 保证数据私有
  • 一定要对数据进行初始化
  • 不要在类中使用过多的基本类型
  • 不是所有字段都需要单独的字段访问器和字段更改器
  • 分解有过多职责的类
  • 类名和方法名要足够体现它们的职责
  • 优先使用不可变的类

第五章 继承

继承的基本思想:基于已有的类创建新的类。就是复用已有类的方法,并且可以增加一些新的方法和字段

类、超类和子类

已存在的类——超类、基类、父类;新类——子类、派生类、孩子类

如Employee中的经理和和员工在薪资待遇上面存在一些差异,但也存在很多相同的地方。他们之间存在一个明显的“is-a”关系,每一个经理都是一个员工:“is-a”关系是继承的明显特征

1
2
3
4
5
6
7
8
9
10
public class Manager extends Employee
{
//added methods and fields
private double bonus;
...
public void setBonus(double bonus)
{
this.bonus = bonus;
}
}

setBonus不是在Employee中定义的,所以Employee不能使用它。经理继承了name、salary、hireDay三个字段,并且新增了bonus字段。

覆盖方法:

如果要返回经理的奖金

1
2
3
4
public double getSalary() 
{
return salary + bonus //不成功
}

因为salary是父类的私有字段,子类Manager的getSalary方法不能直接访问到!

如果我们想调用父类Employee的getSalary方法,而不是当前类的这个方法,可以用super.getSalary()

1
2
3
4
5
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}

这里的super和this不能等同于一类,因为super不是一个对象的引用,例如,不能将值super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

注意:

  • 子类可以增加字段、增加方法或覆盖超类的方法,继承绝不会删除任何字段或方法

    一级标题

    深入理解父子类继承(子类构造器)

有关子类是否继承了父类的私有字段(再理解)

如,Student类继承了Person类

Student对象里,本身就装着一个Person对象。Student对象没有继承Person对象的name字段,所以Student对象没有一个叫name的字段。但Student内部封装的Person对象还是有name字段的。

1
2
3
4
5
public class Person {
private String name;
public Person(String name) { this.name = name; }
public String getName() { return name; }
}
1
2
3
4
5
6
7
public class Student extends Person {
private int id;
public Student(String name, int id) {
super(name);
this.id = id;
}
}

Student没有name字段,但它内部的Person对象有,而且还可以打出来看。

1
2
3
4
5
public static void main(String[] args) {
Student s = new Student("bitch",99);
System.out.println(s.getName()); // BITCH
System.out.println(s.name); // ERROR: name has private access in Person
}

而且注意,我要直接打印Student的name字段 “s.name” ,报错说的是:Person类的name字段为私有,你不可以访问。而不是没有name字段。

大胆一点的话,我们还可以给Student类再加一个name字段。这时候的Student对象本身有一个name字段,内部的基类Person对象还有一个name对象。

1
2
3
4
5
6
7
8
9
public class Student extends Person {
private int id;
private String name;
public Student(String personName, String studentName, int id) {
super(personName);
this.name = studentName;
this.id = id;
}
}

输出:

1
2
3
4
5
public static void main(String[] args) {
Student s = new Student("bitch","whore",99);
System.out.println(s.getName()); // BITCH
System.out.println(s.name); // WHORE
}

注意:

  • 使用super调用构造器,必须是子类构造器的第一条语句
  • 子类构造器如果没有显式地调用超类的构造器,将自动地调用超类的无参数构造器,所以必须要求父类有无参构造,否则报错

多态

1
2
3
4
5
6
7
8
9
10
11
Manager boss = new Manager("Carl Cracker",8000,1987,12,15);
boss.setBonus(5000);

var staff = new Employee[3];

staff[0] = boss;
staff[1] = new Employee("Harry",5000,1989,10,1);
staff[2] = new Employee("Tony",5000,1989,10,1);

for(Employee e:staff)
System.out.println(e.getName() + " " + e.getSalary());

对于e来说,既可以是Manager也可以是Employee,像这种的,一个对象变量可以指示多种实际类型的现象称为多态,在运行时可以自动地选择适当的方法,称为动态绑定

例子:

1
2
3
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;

这里面采用了多态,虽然staff[0]和boss引用同一个对象,但是编译器只将staff[0]看成是一个Employee对象,这意味着,可以这么调用:

1
boss.setBonus(5000); //OK

但不能这么调用:

1
staff[0].setBonus(5000); //Error

这是因为staff[0]的声明类型是Employee,而setBonus不是Employee的方法。setBonus是Manager特有的方法,而不是覆盖重写父类的方法

多态——当声明变量为某一种形态的变量时,编译器就将它看成某种形态。

注意

  • 不能将超类的引用赋值给子类变量,如下非法:
1
Manager m = staff[i]; //Error

原因很清楚:不是所有的员工都是经理,如果赋值成功,m有可能引用了一个不是经理的Employee对象,而在后面有可能会调用m.setBonus,这就会发生错误。

警告:

1
2
Manager[] managers = new Manager[10];
Employee[] staff = managers; //OK

这样是没有问题的,因为manger[i]是一个Manager就一定是一个Employee!一定要切记:这里的staff和mangers引用的是同一个数组,就是一开始new的长度为10的数组!

1
staff[0] = new Employee("Harry");

如果这么去赋值,编译器是可以接受的!但是!!staff[0]和managers[0]是相同的引用,我们把一个普通的员工Harry擅自归入到经理行列(数组)里面去了!!后面如果调用manager[0].setBonus(1000)的时候,将会试图调用一个根本不存在的实例字段,进而搅乱相邻存储空间的内容

牢记:所有数组要牢记创建时候的元素类型,并负责监督仅将类型兼容的引用存储到数组中!例如,使用new managers[10]创建数组是一个经理数组如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常

方法调用

  1. 编译器查看对象的声明类型和方法名。

  2. 确定方法调用中提供的参数类型。

  3. 如果是private、static、final或者构造器,那么编译器将可以准确地知道应该调用哪个方法。——静态绑定;

    动态绑定——如果调用的方法依赖于隐式参数的实际类型,则必须在运行的时候使用动态绑定。

强制类型转换

对于对象:

由于在员工列表中,一部分是纯员工,有一部分是经理(子类),在创建数组的时候申明的是Employee对象,而Employee对象无法读取到其Manager字段或方法等属性(多态),那么在实际用Manager这个对象的时候,要先强制转换成Manager类型:

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

将其复原为Manager对象,以便于访问其额外的字段,如bonus奖金。当然,前提是0号确实是Manager,如果“谎报”,则会报错ClassCastException,为了确保不会谎报,可以先判断一下:

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

受保护字段protected

一般来说,声明为private私有,对其他类都是不可见的,即,子类不能访问超类的私有字段。不过有时候希望限制超类中的某个方法只允许子类访问,或者希望子类的方法访问超类的某个字段

例如,将Employee中的hireDay字段设为protected,而不是private,则Manager方法就可以访问到这个字段。

注意:

  • 要谨慎使用,如果你的代码被别的程序员访问了受保护字段,那么后期维护时候,修改自身类则会影响到别人!
  • 受保护的方法更具有实际意义,表明子类得到了信任,可以正确的使用这个方法,而其他类则不行

泛型类数组列表

ArrayList是一个有类型参数的泛型类。尖括号里面填写保存的元素对象类型,如ArrayList<Employee>

声明一个保存Employee对象的数组列表:

1
2
3
ArrayList<Employee> staff = new ArrayList<Employee>();
//或者
var staff = new ArrayList<Employee>();

也可以省略右边括号里面的类型参数

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

对象包装器与自动装箱

每个基本类型都有与之对应的类Integer、Long、Float、Double、Short、Byte、Character、Boolean;

<>尖括号中的类型参数不允许是基本类型

由于每个值分别包装在对象中,所以ArrayList<Integer>效率远远低于int[]

自动装箱:

1
2
var list = new ArrayList<Integer>()
list.add(3)

此时,进行了自动装箱过程:

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

自动拆箱:此时拿到的n应该是<Integer>类型

1
int n = list.get(i)

转换成:

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