Java基础2

Java基础2

Classes

Classes and Objects

我们常常混淆 类和对象的概念。我们可见简单的将类看作是一个模板。比如说我定义了一个车的类。这个类里面有一些属性(Fields)和一些方法(Methods):

而 Object 是 Class的实例。比如说 Car1,Car2,Car3.这些对象都有 Car Class的属性和方法,但是它们之间又是相互独立的。

Creating Classes

注意,这里我把text初始化为了 “” 这是因为如果不初始化,那么新建对象的时候如果不调用setText, TextBox中的text就会被设置为null,而当我们对null进行字符串操作的时候,整个程序就会down掉

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

public class TextBox {
public String text = ""; //field
//method
public void setText(String text){
this.text = text;
}

public void clear(){
text = "";
}

}

Creating Objects

回到 Main.java 我们可以对刚才创建的类示例化,也就是创建对象

我们不需要 用 TextBox textBox1来新建一个对象,直接 var textBox1 = new TextBox()即可,java会自动判断新建的对象是属于什么类的。

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

public class Main {
public static void main(String[] args) {
var textBox1 = new TextBox();
textBox1.setText("Box 1");
System.out.println(textBox1.text.toUpperCase());

var textBox2 = new TextBox();
textBox2.setText("Box 2");
System.out.println(textBox1.text);
}
}

Memory Allocation

https://www.jianshu.com/p/ef0c80d1782c

  • Java 的内存管理就是对象的分配和释放的处理

1.分配:通过关键字new创建对象分配内存空间,对象存在堆中。
2.释放 :对象的释放是由垃圾回收机制决定和执行的。如果发现某一对象一段时间没有使用过后,就会被自动回收,不需要我们用C++的方法手动释放内存。

  • Java内存泄漏:

当对象存在内存的引用,却不会再继续使用,对象会占用内存无法被GC回收,这些对象就会判定为内存泄漏。

  • Java内存区域划分:

1.栈:
在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配。栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

2.堆:
通过new生成的对象都存放在堆中,对于堆中的对象生命周期的管理由Java虚拟机的垃圾回收机制GC进行回收和统一管理。优点是可以动态分配内存大小,缺点是由于动态分配内存导致存取速度慢。

3.方法区:
是各个线程共享的内存区域,它用于存储class二进制文件,包含了虚拟机加载的类信息、常量(常量池)、静态变量(静态域)、即时编译后的代码等数据。

1.常量池:
常量池在编译期间就将一部分数据存放于该区域,包含以final修饰的基本数据类型的常量值、String字符串。

2.静态域:
存放类中以static声明的静态成员变量。

3.程序计数器
当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。

Procedural Programming

这段代码看起来是面向对象的编程,但是还是属于面向过程的编程。

面向过程的编程会有这样的特点,一个method和class中有很多很多的变量

这样的代码一旦出现了bug就会很难维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.company;

public class Main {
public static void main(String[] args) {
int baseSalary = 50_000;
int extraHours = 10;
int hourlyRate = 20;
int wage = calculateWage(baseSalary,extraHours,hourlyRate);
System.out.println(wage);
}
public static int calculateWage(
int baseSalary,
int extraHours,
int hourlyRate
){
return baseSalary+(extraHours*hourlyRate);
}

}

Encapsulation

为了避免类中成员暴露在外,使得类成员变量被随意赋值或者更改。所以我们需要封装来实现数据的隐藏。应禁止直接访问一个对象中数据的实际表示。

我们把变量和方法都放在了一个类当中,在main类中创建Employee实例。

因为extraHours每个月可能都会不一样,所以我们可以把他作为方法中的一个变量传入。

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

public class Emoloyee {
public int baseSalary;
public int hourlyRate;
public int calculateWage(int extraHours){
return baseSalary+(hourlyRate*extraHours);
}

}

我们看到,一个职员的基本薪资、公式都是public的,这属于商业机密,不能公开,因此需要用private来修饰。如果不用的话,我们在Main类中就可以随意修改Employee的敏感信息了

1
2
3
4
5
6
7
8
9
10
11
package com.company;

public class Main {
public static void main(String[] args) {
var employee = new Emoloyee();
employee.baseSalary = 50_000;
employee.hourlyRate = 20;
int wage = employee.calculateWage(10);
System.out.println(wage);
}
}

因此,我们需要用到 getter和setter,来获取和设置类中的private变量

Getters and Setters - Title

但是像上面那样程序会存在bug,因为我如果把baseSalary =-1,这会出现错误。所以我们在程序当中需要用到一些验证手段。

我们把baseSalary 和hourlyRate

我们可以点击 hourlyRate 这个变量 alt+enter 唤出 encapsulate field hourlyRate 然后出现上面的界面。我们可以把这个变量设置为可读或者可改的。然后idea就会帮我们自己生成这个变量的getter和setter,我们只需要添加我们想要的验证代码即可。

更美妙的是,回到Main.java 我们发现:idea已经自动把刚才的 employee.hourlyRate = 20 替换成了employee.setHourlyRate(20); 美哉

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 com.company;

public class Emoloyee {
private int baseSalary;
private int hourlyRate;

public Emoloyee() {
}

public int calculateWage(int extraHours){
return baseSalary+(getHourlyRate() *extraHours);
}

public void setBaseSalary(int baseSalary){
if(baseSalary <= 0)
throw new IllegalArgumentException("Salary cannot be 0 or less");
this.baseSalary = baseSalary;
}

public int getBaseSalary(){
return baseSalary;
}

public int getHourlyRate() {
return hourlyRate;
}

public void setHourlyRate(int hourlyRate) {
if(hourlyRate<=0)
throw new IllegalArgumentException("hourlyRate cannot be 0 or less");
this.hourlyRate = hourlyRate;
}
}

Abstraction

Reduce complexity by hiding unnecessary details

见JavaScript中的Abstraction

Coupling 耦合度

Coupling is the level of dependency between classes

我们可以将一些没有必要的方法设置为private,把一些必要的methods 暴露出去,这样就可以降低类和类之间的耦合度。

Reducing Coupling

下面是降低耦合度的例子,我们新建一个Browser.java类,我们把没必要暴露的 sendHttpRequestfindIpAddress 这两个 Methods ,所以我们把它设置为 private,这样它们只能在类内被调用,在其它类中无法被调用。只把最重要的 navigate()方法设置为public,这样在 Main class 当中,实例化一个类的时候只能用 navigate这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.company;

public class Browser {
public void navigate(String address){
String ip = findIpAddress(address);
String html = sendHttpRequest(ip);
System.out.println(html);
}

private String sendHttpRequest(String ip) {
return "<html></html>";
}

private String findIpAddress(String address ) {
return "127.0.0.1";
}


}
1
2
3
4
5
6
7
8
package com.company;

public class Main {
public static void main(String[] args) {
var browser = new Browser();
browser.navigate("...");
}
}

Constructors

我们发现,现在的main.java 是这样的:

1
2
3
4
5
6
7
8
9
10
11
package com.company;

public class Main {
public static void main(String[] args) {
var employee = new Emoloyee();
employee.setBaseSalary(50_000);
employee.setHourlyRate(20);
int wage = employee.calculateWage(10);
System.out.println(wage);
}
}

这是一个bad design,因为我们如果要使用employee类,我们就必须要依次调用setBaseSalary和setHourRate方法,否则baseSalary和HourlyRate就是0。

所以我们要简化这个类的接口,这样我们操作起来就比较方便。所以我们就要用到构造函数Constructor,在我们声明新对象的时候就把数据传入。

在Employee类中,我们这样修改:

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
package com.company;

public class Employee {
private int baseSalary;
private int hourlyRate;

public Employee(int baseSalary, int hourlyRate) {
setBaseSalary(baseSalary);
setHourlyRate(hourlyRate);
}

public int calculateWage(int extraHours){
return baseSalary+(getHourlyRate() *extraHours);
}

private void setBaseSalary(int baseSalary){
if(baseSalary <= 0)
throw new IllegalArgumentException("Salary cannot be 0 or less");
this.baseSalary = baseSalary;
}

private int getBaseSalary(){
return baseSalary;
}

private int getHourlyRate() {
return hourlyRate;
}

private void setHourlyRate(int hourlyRate) {
if(hourlyRate<=0)
throw new IllegalArgumentException("hourlyRate cannot be 0 or less");
this.hourlyRate = hourlyRate;
}
}

也就是说我们新建一个构造函数 Employee ,然后在这个构造函数中传入两个参数 baseSalary和hourlyRate,再调用setter方法(因为setter会做一个合法性判断,比直接赋值来的安全)将两个参数的值赋给类中两个成员。此外,我们还需要把两个setter改成private,以防止在调用constructor之后再对baseSalary和hourlyRate两个值进行修改,这样暴露在外的接口就只剩下calculateWage了。

这样在main函数中代码可以变得更为精简

1
2
3
4
5
6
7
8
9
10
11
package com.company;

public class Main {
public static void main(String[] args) {
var employee = new Employee(
50_000,
20);
int wage = employee.calculateWage(10);
System.out.println(wage);
}
}

和C++一样,如果我们不声明一个构造函数,那么java会自动帮我们创建一个缺省的构造函数。他会帮我们初始化成员值:int 类型的都设置为0 ,Boolean类型的都设置为False等。

Method Overloading

方法重载,我们可以用这种办法定义很多方法名字相同但是参数不同的方法,比如说现在我受伤的calculateWage方法,通过重载,我可以既传入一个extraHours参数,也可以什么都不传,代表着extraHours=0。代码如下:

1
2
3
4
5
6
public int calculateWage(int extraHours){
return baseSalary+(getHourlyRate() *extraHours);
}
public int calculateWage(){
return baseSalary;
}

当然我们不建议多次重载,这样会让软件很难维护。通常一个方法需要接受多种不同类型的参数的时候才会用到重载。

Constructor Overloading

我们也可以重载构造函数。如下:

利用this(),直接调用构造函数。

1
2
3
4
5
6
7
public Employee(int baseSalary, int hourlyRate) {
setBaseSalary(baseSalary);
setHourlyRate(hourlyRate);
}
public Employee(int baseSalary ) {
this(baseSalary,0);
}

Static Members

静态成员,包括静态方法和静态变量,是只能在类中实现的,它们不属于任何一个实例对象。

比如我可以新建一个 Employee.numberOfEmplotees和printNumberOfEmployees方法来计算和输出Employee的个数,但这个属性不属于任何一个实例Employee对象而是属于这个Employee类

1
2
3
4
5
6
7
8
9
public static int numberOfEmployees;
public static void printNumberOfEmployees(){
System.out.println(numberOfEmployees);
}
public Employee(int baseSalary, int hourlyRate) {
setBaseSalary(baseSalary);
setHourlyRate(hourlyRate);
numberOfEmployees++;
}

但是要注意,我们在static 方法当中不能调用类中的出static方法以外的其他方法,如calculateWage().这也是为什么我们一开始在Main类当中添加的方法都被声明为static方法,这是为了在main方法中能够调用他们(都是static可以调用)

那么为什么main方法是静态的呢?这是为了能让Java能不创建一个新的对象,而直接调用这个main方法

我们同样可以发现,System 类中也有很多很多的静态方法:System.out,System.in 又比如说 Interger.parseInt()(转换为整数)也属于静态方法

Classes Quiz

1- A class is a blueprint or template for creating objects. An object is an instance of a class.
2- Instantiating means creating an instance of a class: new Customer()
3- Stack is used for storing primitive types (numbers, boolean and character) and variables that store references to objects in the heap. Variables stored in the stack are immediately cleared when they go out of scope (eg when a method finishes execution). Objects stored in the heap get removed later on when they’re no longer references. This is done by Java’s garbage collector.
4- Big classes with several unrelated methods focusing on different concerns and responsibilities. These methods often have several parameters. You often see the same group of parameters repeated across these methods. All you see is procedures calling each other passing arguments around.
By applying object-oriented programming techniques, we extract these repetitive parameters and declare them as fields in our classes. Our classes will then encapsulate both the data and the operations on the data (methods). As a result, our methods will have fewer parameters and our code will be cleaner and more reusable.
5- Encapsulation is the first principle of object-oriented programming. It suggests that we should bundle the data and operations on the data inside a single unit (class).
6- How we store data in an object is considered an implementation detail. We may change how we store the data internally. Plus, we don’t want our objects to go into a bad state (hold bad data). That’s why we should declare fields as private and provide getters and or setters only if required. These setters can ensure our objects don’t go into a bad state by validating the values that are passed to them.
7- Abstraction is the second principle of object-oriented programming. It suggests that we should reduce complexity by hiding the unnecessary implementation details. As a metaphor, think of the remote control of your TV. All the complexity inside the remote control is hidden from you. It’s abstracted away. You just work with a simple interface to control your TV. We want our objects to be like our remote controls.
8- Coupling represents the level of dependency between software entities (eg classes). The more our classes are dependent on each other, the harder it is to change them. Changing one class may result in several cascading and breaking changes.
9- By hiding the implementation details, we prevent other classes from getting affected when we change these details. For example, if the logic board and transistors inside a remote control change from one model to another, we’re not affected. We still use the same interface to work with our TV. Also, reducing these details and exposing fewer methods makes
our classes easier to use. For example, remote controls with fewer buttons are easier to use.
10- Constructors are called when we instantiate our class. We use them to initialize our objects. Initialization means putting an object into an early or initial state (eg giving it initial values).
11- Method overloading means declaring a method with the same name but with different signatures. The number, type and order of its parameters will be different.
12- Static methods are accessible via classes, not objects

Refactoring Towards an Object-oriented Design

The Problem

我们看到这个程序:

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
package com.company;

import java.text.NumberFormat;
import java.util.Scanner;

public class Main {
final static byte MONTHS_IN_YEAR = 12;
final static byte PERCENT = 100;

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);
int principal = (int) readNumber("Principal: ",1000,1000000);
float annualInterest = (float)readNumber("Annual Interest Rate: ",1,30);
byte years = (byte)readNumber("Period (Years): ",1,30);

double mortgage = calculateMortage(principal,annualInterest,years);
String mortgageFormatted =
NumberFormat.getCurrencyInstance().format(mortgage);
System.out.println();
System.out.println("MORTAGE");
System.out.println("------");
System.out.println("Mortgage: " + mortgageFormatted);

System.out.println();
System.out.println("PAYMENT SCHEDULE");
System.out.println("------");
for(short month = 1;month<= years*MONTHS_IN_YEAR;month++){
double balance = calculateBalance(principal,annualInterest,years,month);
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}

}

public static double readNumber(String prompt,double min,double max){
Scanner scanner = new Scanner(System.in);
double value;
while (true) {
System.out.print(prompt);
value = scanner.nextInt();
if (value >= min && value <= max)
break;
System.out.println("Enter a value between "+min+" and "+max);
}
return value;
}

public static double calculateBalance(
int principal,
float annualInterest,
byte years,
short numberOfPaymentMade) {

short numberOfPayments = (short)(years * MONTHS_IN_YEAR);
float monthlyInterest = annualInterest / PERCENT / MONTHS_IN_YEAR;

double balance = principal*(Math.pow(1+monthlyInterest,numberOfPayments)-Math.pow(1+monthlyInterest,numberOfPaymentMade))/(Math.pow(1+monthlyInterest,numberOfPayments)-1);
return balance;
}

public static double calculateMortage(
int principal,
float annualInterest,
byte years){

short numberOfPayments = (short)(years * MONTHS_IN_YEAR);
float monthlyInterest = annualInterest / PERCENT / MONTHS_IN_YEAR;

return principal
* (monthlyInterest * Math.pow(1 + monthlyInterest, numberOfPayments))
/ (Math.pow(1 + monthlyInterest, numberOfPayments) - 1);
}
}

虽然这个程序可以正常运行,但是可以发现这个程序非常过程化,里面的methods都是互不联系的。我们既没有encapsulation,也没有abstraction 。我们现在我们需要把它重构成面向对象的代码。

What Classes Do We Need?

对于这个程序,我们需要设一个什么类呢?

比如 principal、annualInterest、years 这三个需要被读入的数据,我们可以建一个console class, 专门从命令行中读取数据。我们还有printMortgage、printPaymentSchedule这些用来打印数据的方法,我们可以新建一个 Mortgage report类来把这些方法放在那。注意,这两个方法不要放到console class当中去,因为console class只管读入数据,对于内部程序的运行尽量不要参与。

对于calculatebalance和calculateMortage这两个方法,传入的参数有三个相同,所以我们可以把他们放到一个类中,并由类来管理这三个相同的数据。

Extracting the Console Class

我们利用Idea给的重构工具可以很方便的将一个method变成一个类。点击readNumber这个方法,然后点击Refactor,选Move Members,选择public

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Console {
public static double readNumber(String prompt, double min, double max){
private static Scanner scanner = new Scanner(System.in);
double value;
while (true) {
System.out.print(prompt);
value = scanner.nextInt();
if (value >= min && value <= max)
break;
System.out.println("Enter a value between "+min+" and "+max);
}
return value;
}
}

Overloading Methods

我们对Console类进行重载,因为我们想要一个仅仅返回一个数字的readNumber方法。因为两个readNumber 都需Scanner,所以直接将Scanner提出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Console {
private static Scanner scanner = new Scanner(System.in);
public static double readNumber(String prompt){
return scanner.nextDouble();
}
public static double readNumber(String prompt, double min, double max){
double value;
while (true) {
System.out.print(prompt);
value = scanner.nextInt();
if (value >= min && value <= max)
break;
System.out.println("Enter a value between "+min+" and "+max);
}
return value;
}
}

Extracting the MortgageReport Class

现在我们要将main函数中打印利息和每月还剩余代还金额的代码提出来单独成一个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void printMortgage(int principal,float annualInterest,byte years){
double mortgage = calculateMortage(principal,annualInterest,years);
String mortgageFormatted =
NumberFormat.getCurrencyInstance().format(mortgage);
System.out.println();
System.out.println("MORTAGE");
System.out.println("------");
System.out.println("Monthly Payments: " + mortgageFormatted);
}
private static void printPaymentShedule(int principal,float annualInterest,byte years){
System.out.println();
System.out.println("PAYMENT SCHEDULE");
System.out.println("------");
for(short month = 1;month<= years*MONTHS_IN_YEAR;month++){
double balance = calculateBalance(principal,annualInterest,years,month);
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}
}

选中这两个方法后,我们选择重构中的 Convert to instance method ,如下图:

然后我们创建一个新的 Class并将这两个方法选中,设置为Public

结果如下图所示:

但是我们注意到 printMortage 中 calculateMortage函数是引用的Main class中的方法,这是不应该出现的。我们需要将Mainclass中的两个计算方法独立出来成为一个类。

Extracting the MortgageCalculator Class

现在我们选中两个 calculate方法并新建一个 MortgageCalculator 类。

新建完成后,结果如下:

这还没完,我们之前学了encapsulation,也就是将方法中的变量放到类中,并设置 constructor或者getter/setter .而不是在引用方法的时候将参数传入。因此我们可以对代码进行如下修改:

首先我们将方法中需要的变量 principalannualInterestyears 定义在class当中,设置为 private

然后我们可以选择快捷方式:在code菜单中下拉到generate,然后点击 constructor

然后,Java就自动为我们创建好了 这个类的构造器:

1
2
3
4
5
6
7
8
9
10
11
12
public class MortgageReport {
private int principal;
private float annualInterest;
private byte year;

public MortgageReport(int principal, float annualInterest, byte year) {
this.principal = principal;
this.annualInterest = annualInterest;
this.year = year;
}
//...
}

紧接着,我们可以通过 Idea的快捷方法将calculateBalance这些方法中使用到的变量改为本地变量。在 Refactor中选择 Change Signature

在界面中,我们将这三个由构造器固定下来的参数删除即可

删除之后我们发现,这些变量无法被识别。这是因为我们的 calculateBalance是 静态函数,静态函数只能识别出静态的 Fields ,因此,我们还需要重构一下,将方法转换成 Instance Method,也就是将 static关键字删除。

结果如下:

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
package com.company;

public class MortgageCalculator {
private int principal;
private float annualInterest;
private byte years;

public MortgageCalculator(int principal, float annualInterest, byte years) {
this.principal = principal;
this.annualInterest = annualInterest;
this.years = years;
}

public double calculateBalance(short numberOfPaymentMade) {
short numberOfPayments = (short)(years * Main.MONTHS_IN_YEAR);
float monthlyInterest = annualInterest / Main.PERCENT / Main.MONTHS_IN_YEAR;
double balance = principal*(Math.pow(1+monthlyInterest,numberOfPayments)-Math.pow(1+monthlyInterest,numberOfPaymentMade))/(Math.pow(1+monthlyInterest,numberOfPayments)-1);
return balance;
}

public double calculateMortage(){
short numberOfPayments = (short)(years * Main.MONTHS_IN_YEAR);
float monthlyInterest = annualInterest / Main.PERCENT / Main.MONTHS_IN_YEAR;
return principal
* (monthlyInterest * Math.pow(1 + monthlyInterest, numberOfPayments))
/ (Math.pow(1 + monthlyInterest, numberOfPayments) - 1);
}
}

Moving Away from Static Members

但是问题又出现了,我们删除了 static关键字之后,我们就不能直接用 类名.方法 的形式来引用我们的calculateBalancecalculateMortage方法了。因此我们必须找出这两个方法在别的类中的引用,并修改。

我们可以通过右键方法名称,Find usages快速找到方法在其它类中被引用的地方。发现是在 MortgageReport中。不能直接用 . 来访问,那么我们就不得不新建一个MortgageCalculator实例。

问题又出现了,我不想在两个方法中都新建一个MortagageCalculator实例,因此我希望直接将其定义成类中的 Field

结果如下:

但我还是不满意,因为这样子我们还是不得不将我们的calculator对象放在 printMortage方法当中.

在这个 MortgageReport 类当中,两个方法只用到了一次 years变量,因此我们只需要为years设置一个 getter, 然后将 calculator作为一个参数传入到MortageReport这个类当中去。 因此我们可以为这个class设置一个 Constructor。然后将两个方法从static method 改成 instance method.

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 com.company;

import java.text.NumberFormat;

public class MortgageReport {
private static MortgageCalculator calculator;

public MortgageReport(MortgageCalculator calculator) {
this.calculator = calculator;
}


public void printPaymentShedule(){
System.out.println();
System.out.println("PAYMENT SCHEDULE");
System.out.println("------");
for(short month = 1; month<= calculator.getYears()* Main.MONTHS_IN_YEAR; month++){
double balance = calculator.calculateBalance(month);
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}
}


public void printMortgage(){
double mortgage = calculator.calculateMortage();
String mortgageFormatted =
NumberFormat.getCurrencyInstance().format(mortgage);
System.out.println();
System.out.println("MORTAGE");
System.out.println("------");
System.out.println("Monthly Payments: " + mortgageFormatted);
}
}

将这里的方法改成实例方法(Instance method)之后,Main class中的代码也需要随之更改了:

Moving Static Fields

现在我们的程序结构变得越来越严谨和简洁了。但是我们发现Main CLass中还是有噪音。那就是 开头的两个 静态变量。很显然,这两个静态变量是不属于Main函数的,放在这里很突兀。因此我们需要给它们找到合适的归宿。

查看这个MONTHS_IN_YEAR 的用法,我们发现,在MortageCalculator类中用到两次,在MortageReport类中用到一次。

因此我们应该将这两个静态变量移动到 MortageCalculator 类当中去,然后让MortageReport引用它。注意,这里要设置public

结果如下:

Extracting Duplicate Logic

现在,我们将焦点放到 MortgageCalculator 类上。

我们发现, calculateBalance和 calculateMortgage 这两个方法的内部逻辑都差不多。比如,numberOfPaymentsmonthlyInterest 都是重复计算的。因此我们可以通过一些方法来简化这两个函数

因此,将这两个变量的计算抽象成单独的 Private Method是一个不错的方法。

将两个变量都操作完之后,我们得到了两个类似于getter的函数(因为是private的,所以和public的getter还是有区别的)。一般,我们把getter和setter都放到整个类的最底部。如下图所示:

这样设计的好处,是分工特别明确。 MortagageCalculator只需要负责计算就可以了,当参数进行修改之后,不需要再通过复杂的传入就能实现计算。

Extracting getRemainingBalances - Title

现在还有一个问题,就是 在 MortgageReport 类中的 printPaymentShedule()方法

我们发现这个for循环在这里就显得比较突兀。因为这个方法 printPaymentShedule的功能只是打印结果,而不应该有计算功能。因此我需要将这段代码抽象出来,放到负责计算的MortgageCalculator类当中去

MortagageCalculator 中我们创建一个这样的方法:

1
2
3
4
5
6
7
public double[] getRemainingBalances(){
double[] balances = new double[getNumberOfPayments()];
for(short month = 1; month<= balances.length; month++){
balances[month-1] = calculateBalance(month);
}
return balances;
}

这个方法返回一个double类型的数组。

1
2
3
4
5
6
7
public void printPaymentShedule(){
System.out.println();
System.out.println("PAYMENT SCHEDULE");
System.out.println("------");
for (double balance: calculator.getRemainingBalances())
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}

因为在 printPaymentShedule中没有用到 getYears方法了,我们在calculator类中删除这个方法。

此外,我们也没有在MortgageReport类中用到 MONTHS_IN_YEARPERCENT这两个变量,因此我们可以将其改为private,以将Calculator类变得更加鲁棒。

One Last Touch

最后,我们发现, MortgageReport类中的 NumberFormat.getCurrencyInstance()还可以变得更简单一些,于是,我们将其定义为这个类中的一个变量。如下图所示

Inheritance

Inheritance

继承,顾名思义就是能让我们重复利用一些代码。比如说我们要设计一组表单。那么我们可以参考这样的设计语言:

首先我们新建一个java类 UIControl

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

public class UIControl {
private boolean isEnabled = true;
public void enable(){
isEnabled = true;
}
public void disable(){
isEnabled = false;
}
public boolean isEnsabled(){
return isEnabled;
}
}

现在我们再新建一个TextBox 类:我们用 子类 extends 父类 来表示继承语法

然后,这个子类就拥有了父类中的三个public方法: enable(),disable(), isEnsabled()

The Object Class

我记得在 https://jasonxqh.github.io/2020/05/26/OOP-in-JavaScript/ 中讲过, 在Javascript中所有的对象都有一个对象的子对象。这个对象就叫做 Object Class .在 Java中也是这样。Java中的任何一个类都是由Object类扩展而来,但不用写class a extends Object,无论是否指出,Object都被认为是此类的直接超类或间接超类。所以任何类都可以调用Object类中方法

比如说 hashcode()方法

注意了,hashcode是每一个类专属的编码。如果我们直接用equals 方法比较 两个 TextBox实例,发现是false的。以后我们会将怎么通过比较类中的方法、参数来判断它们是否会向等。

Constructors and Inheritance

Java类中的布局一般是: 前面放参数,然后放构造函数,最后放一些 方法。

和C++、Javascript中一样,创建一个子类的实例对象的时候,会先调用其父类的构造函数,然后调用子类的构造函数。

结果是先打印 UIControl,再打印TextBox。

但是如果我们再UIControl的构造器中加入一个setter:

那么这时候在 TextBox中的构造器就会报错。因为这时候父类已经不是默认构造函数了,而是有参构造函数。因此,我们想要调用子类的构造函数 ,就要手动调用父类中的构造函数,语法如下:

注意,这里 super(参数) 一定要写在构造器的第一行,位置改变就会报错。

Access Modifiers

我们只要深刻了解这张表格就可以了,基本上和 C++ 是一样的。

但是Protected不建议使用,因为界定不明确,容易误用。

Overriding Methods

在java中,方法覆盖(重写)的实现是在子类中对从父类中继承过来的非私有方法的内容进行修改的一个动作。比如说我想在 TextBox中写一个函数 toString() 而这个函数其实是Object 类中的一个方法。因此我们就要使用 Method Overriding

Upcasting and Downcasting

Upcasting就是向上转型,Downcasting就是向下转型

在C++中我们提到过向上转型和向下转型。向上转型,就是java多态中的父类引用指向子类对象。但要注意的是 父类引用不可以访问子类新增加的成员(属性和方法)。换句话说,就是把子类类型转换为父类类型

向下转型则是相反,将一个对象”下放”成它的一个子类对象。

这里有一个例子,我们创建了一个 UIControl对象和TextBox对象。然后对其使用 show() 函数,我们发现堆textBox这个对象,使用 println()方法是无法打印出东西的。这是为什么?

因为show函数传入的参数是一个Object对象,打印需要使用到对象的toString()函数,但是我们之前将textBox类中的toString()函数给重写了,并让其返回textBox中的参数text,然而此时我们仅仅是声明了textBox对象,并未设置其text属性,因此打印出来的是一个空字符串

然而我们要么在show方法的外面通过textBox.setText("")来设置text,要么在show函数里面设置text。但是问题又出现了。如果我们在show方法中直接使用 control.来访问,发现并没有setText("")函数,因为Object是 TextBox class的祖先对象,TextBox class中的方法并不会出现在Object class当中 。这时候我们就需要选择向下转型了:

但是注意了,这边我们可以使用show(textBox)吗? 答案是不行的:因为每一个textBox都是一个Object对象,但是不一定每个Object的对象都是textBox. 所以这里如果传进去一个非TextBox的实例,就会报错。

报错信息:class com.company.UIControl cannot be cast to class com.company.TextBox

Comparing Objects

现在我新建一个类:

1
2
3
4
5
6
7
8
9
10
11
package com.company;

public class Point {
private int x;
private int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

同时,我们在 main函数中新建两个 Point对象,它们有相同的x、y值,然后我们可以通过 == 或者 equals()来比较它们是否相等。

1
2
3
4
5
6
public static void main(String[] args) {
var point1 = new Point(1,2);
var point2 = new Point(1,2);
System.out.println(point1==point2);
System.out.println(point1.equals(point2));
}

结果是两个 false,因为不管是 == 还是默认的 equals() 都是比较这两个对象的引用。所以我们希望重写这个类的 equals() 方法,然后将其改为比较两个对象的 x、y值是否相等。

我们可以这样重写 equals函数。注意了,首先我们需要将Object类的参数obj 向下转型成当前的TextBox类,因为传入的参数是无法改变的(否则就是重载函数了)。

1
2
3
4
5
@Override
public boolean equals(Object obj) {
var other = (Point)obj;
return other.x==x && other.y==y;
}

现在再运行 point1.equals(point2) ,结果为true

但这样写,是有一个bug的,那就是如果我 point1.equals(new TextBox()) 的话 ,TextBox显然不能被强制转换成 Point类,因此,整个程序就crush了。

为了解决这个问题,我们还是可以选择条件语句:

1
2
3
4
5
6
public boolean equals(Object obj) {
if (!(obj instanceof Point))
return false;
var other = (Point)obj;
return other.x==x && other.y==y;
}

那么问题来了,如果我直接 point1.equal(point1) 会怎么样?毫无疑问是返回true的,但还是通过比较other.x==x && other.y==y; 来实现的,这样程序的性能就会变差。

我们可以先比较一下他们的reference是否相同。如果相同就直接返回true。

此外,我们在重写 equals()的同时也要重写hashcode方法。

我们先来看一下Object.hashCode的通用约定
  1.在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同,即这个应用程序这次执行返回的整数与下一次执行返回的整数可以不一致。
  2.如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
  3.如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一个对象的hashCode方法,不要求必须产生不同的整数结果 。然而,程序员应该意识到这样的事实,对于不相等的对象产生截然不同的整数结果 ,有可能提高散列表(hash table)的性能。

  如果只重写了equals方法而没有重写hashCode方法的话,则会违反约定的第二条:相等的对象必须具有相等的散列码(hashCode)

因为:两个对象相等,hashcode一定相等

   两个对象不等,hashcode不一定不等

   hashcode相等,两个对象不一定相等

   hashcode不等,两个对象一定不等

我们可以直接通过 Idea内置的方法来帮我们生成这两个函数:在generate中点击 equals和hashCode ,就可以生成了:Java帮我们自动生成的代码覆盖面更广,更全面。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x &&
y == point.y;
}

@Override
public int hashCode() {
return Objects.hash(x, y);
}

Polymorphism

Polymorphism就是多态。所谓多态,就是同一个行为具有多个不同表现形式或形态的能力。比如说下面这个例子,TextBox() CheckBox() 都是继承自 UIControl 的,但是我们希望当controls数组中的元素是TextBox()时,就渲染出TextBox的内容,反之,如果时CheckBox,那么就渲染出CheckBox。这样我们就可以不使用 if语句。

为了实现这个方法,我们首先在 UIControl 中新建一个 render方法。然后我们分别在TextBox和CheckBox中重写这个render方法:

1
2
3
4
5
6
public class CheckBox extends UIControl{
@Override
public void render() {
System.out.println("Render CheckBox");//System.out.println("Render TextBox");
}
}

最后我们在main函数中这样写,结果是我们想见到的。:

Abstract Classes and Methods

抽象类和抽象方法就是不能被实例化的类。比如说这里的 UIControl 方法。UIControl 是一种概念,一种共同属性的抽象几何,因此我们可以用abstract 关键字来定义抽象类。抽象类的作用仅仅是表达一个接口,并不是写具体的实现细节。抽象类中可以存在抽象方法,抽象方法也是使用 abstract 关键字来修饰的。抽象的method是不完全的,他只有声明,没有 大括号{}

这里我们堆UIControl类进行一个改造

当我们声明了一个类为 抽象类,并在这里声明了一个抽象函数的时候。他的子类要么是继承这个抽象类的,要么是将这个类中的抽象方法进行重写。否则会报错

Final Classes and Methods

我们知道 抽象类是不能实例化的,他们只能被继承。 Final则相反。当我们声明一个类是final class的时候,他就不能再被继承了。

一般来说我们不要轻易得用 final去修饰一个类。因为这就堵住了这个类被继承的可能性。

当我们 将一个 方法定成 final method的时候,这个方法就不能再被重写了

Deep Inheritance Hierarchies

注意,不要一直使用继承结构!

比如说这个情况,这样一直继承很多代就会导致代码的耦合度非常非常高,后期维护起来异常困难。

所以这里 虽然 Instructor和Student之间可能存在相同的逻辑,但是他们也不应该通过继承联系起来。

Multiple Inheritance

在Java中,不像C++,可能有多个 Parent对象。 菱形继承在Java中是不可行的。

Interfaces

What are Interfaces

Interfaces (接口) 在 JAVA中是一个抽象类型。它可以帮助我们构建低耦合的、易继承的、易测试的软件。

因为我们希望类与类之间的耦合度降到最低,这样我们就可以尽可能少的“牵一发而动全身”

原来A、B两个类可能是这样的:

但是使用了 Interface之后,他们的关系就成了这样:

也就是说,现在修改B甚至替换B,不会对A造成任何影响

接口与类相似点:

  • 一个接口可以有多个方法。
  • 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
  • 接口的字节码文件保存在 .class 结尾的文件中。
  • 接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

  • 接口不能用于实例化对象
  • 接口没有构造方法
  • 接口中所有的方法必须是抽象方法
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现
  • 接口是说:应该做什么 ,而类是说:应该怎么
  • 接口支持多继承

比如说,我在接口中定义了一个 Sorting,那么我们就可以在不同的类中实现不同的排序算法。

又比如说,税率计算器,我在接口中定义了一个计算器,但是每年税务政策不同,我就要用不同的 类来实现不同的计算方法

Tightly-coupled Code

现在我声明两个类:TaxCalculatorTaxReport

1
2
3
4
5
6
7
8
9
10
11
12
package com.company;

public class TaxCalculator {
private double taxableIncome;

public TaxCalculator(double taxableIncome) {
this.taxableIncome = taxableIncome;
}
public double calculateTax(){
return taxableIncome*0.3;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.company;

public class TaxReport {
private TaxCalculator calculator;

public TaxReport(){
calculator = new TaxCalculator(100_000);
}
public void show(){
var tax = calculator.calculateTax();
System.out.println(tax);
}
}

我们发现这两个类实际上是高耦合的。

比如说, TaxReport 中新建了 TaxCalculator 实例,导致当 TaxCalculator修改变量个数时,TaxReport 就会报错。

Creating an Interface

现在我们新建一个 Interface

然后将原来的 TaxCalculator 改为 TaxCalculator2020

一般来说,Interface的命名规则是加上一个 前缀 can 或者 I, 或者是 **able 之类的词语。

因为这是一个接口,只做方法声明,并且这个方法是要被其他的类实现的,必然是public的。所以,在声明方法的时候并不需要加 public。

TaxCalculator2020这个类中调用 接口,我们需要这样来写:

1
public class TaxCalculator2020 implements TaxCalculator{...}

如果还有继承,那么 我们一般把继承写在引入接口的前面:

1
public class TaxCalculator2020 extends Object implements TaxCalculator{...}

在实现接口中的函数的时候(这里时 calculateTax) 我们最好在前面写一个 @Override ,这样,当接口中这个方法被删除的时候,@Override 就会报错,作为一个提醒。

接下来,我们要在 TaxReport 类中使用接口

Dependency Injection

现在我们来讲一个很重要的概念: 依赖嵌入(Dependency Injection)

这个意思是说,我们的类不应该实例化他们的依赖项(dependencies)

也就是说 TaxReport这个类不应该去创建一个实例化的Calculator对象出来,而是仅仅使用它。Injection的意思时注射,这非常形象,我们要做的就是通过某种方法来给TaxReport 注射一个Calculator实例化对象。

我们有很多的方法来实现这一想法:下面会一一介绍 Constructor Injection,Setter Injection以及 Method Injection

Constructor Injection

我们先用 Constructor 来实现 Dependency Injection.

对于刚才耦合度很高的 TaxReport 代码,我们做出如下修改:

我们将实例化的 TaxCalculator2020改成了调用一个接口 TaxCalculator ,然后直接调用 TaxCalculator中的 calculateTax() 代码。

这样我们在main中可以声明一个 TaxCalculator2020 的实例化对象,然后传入到 TaxReport 的构造函数中去,但是完全没有报错。

这就叫 Programming against interface

通过这样的技巧,当TaxCalculator 中的方法进行修改,或者新创建了一个实现接口的类TaxCalculator2019 ,都不会影响到 TaxReport ,这个类不需要重新编译了,因为他引用的只是一个接口罢了。

在这个简单的程序中我们只有2个类,但是在大型的项目中,可能有成百上千的类。并且这些类可能都含有很多的 dependency 那么这时候在main函数中自己申明这么多对象就显得不现实了。因此,spring等框架就是帮我们减少这部分的工作量的。

Setter Injection

同样的,我们可以通过setter来 给 TaxReport类注射实例化对象,首先我们必须在TaxReport 中创建一个setter

然后,我新建了一个 TaxCalculator2019 的类,并将其通过 setter注射给了report

相比于 setter Injection ,Constructor Injection是一种更加普遍的方法。

Method Injection

Method相比于前两种方法更为精简。也就是将 实例化对象 直接通过一个方法注射进 TaxReport类当中去。

比如说,我这样修改我的 show()函数:

然后在 Main()函数中,我们就直接通过 show()传对象就行了。

对于Method Injection ,Constructor Injection 可以更明显的告诉我们两个类之间的 dependency,因为我们是通类的构造函数传入的,非常直观。

虽然现在我们的耦合度已经很低了,但是class或多或少还是和 Interface存在着一些关联性。所以在设计接口的时候,我们要尽量轻量化、简洁化,不要搞一个很重型的接口。

Interface Segregation Principle

接口分离原则,简单来说就是将“大(函数多、复杂)” 的接口拆分成一些更小、更轻量化的接口

比如说我这里新建了一个 UIWidget 类,主要是一些窗口小部件之类的(如 text boxes) ,然后在其中声明三个方法: drag, resize,render

然后我又去新建了一个 Dragger 类,然后我们要用到UIWidget接口中的 drag方法。如下图所示:

但是这样的话,就会显得耦合度过高。比如说,我在 UIWidget 中修改 resize() ,那么即使我们没有修改任何关于drag()中的内容,drag()以及 Dragger 中的也会重新编译一遍。这还是只有三个方法的接口,那么当一个接口中有十几二十个方法的时候,修改接口中的一个方法,波及到用到接口的所有的类以及类的子类们,这个代价恐怕是非常大的。因此我们可以将接口拆封成几个更小的接口,如下图所示:

我们将Drag() 单独拉出来成立一个 接口。然后在Dragger类中只使用Draggable接口,这样就降低了很多耦合的情况。

如果我觉得一定要将这三个功能(drag,resize,render)放在一起,我们可以使用 接口 extends 接口的操作:

这样 UIWidget 就会继承 Draggable 接口,并且我在Dragger中传入UIWidget对象也不会报错。

接下来我们使用 Idea的内置方法来帮助我们自动生成接口:

选中 resize 方法,用 control+t 快捷键打开Refactor菜单,向下找到 Extract Interface选项,点击并重命名新的接口名称为 Resizable

然后IDEA会提醒我们说: IDEA可以分析UIWidget的用法,并在可能的情况下将其替换为Resizable的用法,我们选择不需要。结果如下:

但是 Interface segregation 并不是要求我们将每一个函数都独立成一个接口。而是让我们要将不同功能的方法给区分开来。比如说在这个例子里,对一个对象来说,resizing和redragging 是两种不同的操作。但是在resize中我们有可能存在很多不同的方法:比如void resize(int x,int y)(通过坐标修改),resizeTo(UIWidget widget) 将一个对象的形状赋值给另一个对象等 这些都可以放到同一个接口当中。

我们还注意到一个接口可以继承多个接口,而在类中这是不能实现的。

Fields in Interfaces

最近几年,Java的接口多了很多新的特性。但是这并不是一个很好的发展方向。我们在使用这些新的特性的时候,必须清楚自己到底在干什么,合不合理,否则不要滥用。

第一个特性就是在接口中定义参数。

这时候的参数,默认是一个常数,是不能被改变的。但这个功能比较鸡肋,尽量不要使用。因为如果修改了这个参数,所有与其相关的类都会受到影响。

Static Methods

还是一样的道理,我们如果修改了method内部的逻辑,就会影响到与接口相关的所有类。

Private Methods

私有方法在接口中就显得更加突兀了。这应该是细节,并不是要在接口这么抽象的东西中实现。

Interfaces and Abstract Classes

62

现在我们来谈谈 接口和抽象类之间的区别。以前,接口只做声明、没有具体代码和执行操作。我们用其实现低耦合的、易继承的、易测试的软件。但是抽象类 则是在不同的类之间共享一段相似的代码。两者的区别是十分清晰的。但是现在,接口中出现了很多不好的特性:静态函数、私有函数、变量等。让他越来越像是一个类了,此外,由于接口的多继承特性,它常常被用来hack 掉java不能多继承的原则。如下图:

62

再说一遍,接口不要和类混为一谈!

When to Use Interfaces

  • Swap Implementations: 自从利用了接口之后,我们就降低了类与类之间的耦合度。对于一个app来说可以很容易的切换它所需要用到的服务。比如这里,当使用不同的 VideoEncoder的服务,我们就可以直接修改Encoder当中的内置算法,但对VideoProcessor并不会产生影响,对于 VideoProcessor来说,接口好像是一个黑匣子,只要把一个对象传进去就可以了。

62

  • Extend Your Applications: 当我们设计了一套框架,想给别人用的时候,也需要用到接口。比如说我们有这样一个逻辑:我们的框架应该有一个 模板引擎来处理客户端发来的http请求。这时候我们就可以使用接口,来让我们的http反馈通过接口传给用户。这样,用户也能通过其他的模板引擎处理http请求。

62

  • Test Your Class in Isolation

如果我们的类在使用 邮箱服务或者是存储引擎,我们可以通过接口来单独测试这个类。这就是我们所说的 单元测试。

-------------本文结束,感谢您的阅读-------------