Java修炼入门P3

面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,Java是面向对象的。面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。

对象与类


OOP概述

1.类(class)是构造对象的模板或蓝图。(用Java编写的所有代码都位于某个类的内部)

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

3.封装(encapsulation,有时称为数据隐藏)是与处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏了具体的实现方式。

4.对象中的数据称为实例字段(instance field)操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特定的实例字段值。这些值的集合就是这个对象的当前状态(state)。无论何时,只要在对象上调用一个方法,它的状态就有可能发生改变。

5.实现封装的关键在于,绝对不能让类中的方法直接地访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。

6.在Java中,所有的类都源自于Object类。其他所有类都扩展自这个Object类。

7.在扩展一个已有的类时,这个扩展后的新类具有所扩展的类的全部属性和方法。只需要在新类中提供适用于这个新类的新方法和数据字段就可以。通过扩展一个类来建立另外一个类的过程称为继承(inheritance)

对象

1.对象的特性:

  • 对象的行为(behavior)(可对象完成哪些操作,或可对对象应用哪些方法)
  • 对象的状态(state)(调用方法时对象如何响应)
  • 对象标识(identity)(区分具有相同行为与状态的不同对象)

2.同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法定义的。

3.每个对象都保存着描述当前状况的信息,即对象的状态。对象状态的改变必须通过调用方法实现。(如果不经过方法调用就可以改变对象状态,只能说明破坏了封装性)

4.对象的状态并不能完全描述一个对象。每个对象都有一个唯一的标识(identity,或称身份)。需要注意,作为同一个类的实例,每个对象的标识总是不同的,状态也往往存在着差异。

5.对象的这些关键特性在彼此相互影响。例如,对象的状态也会影响它的行为。


识别类

1.首先从识别类(在分析问题的过程中寻找名词,而方法对应着动词)开始,然后再为各个类中添加方法。


类之间的关系

1.类之间最常见的关系有:

  • 依赖(dependence "uses-a"):如果一个类的方法使用或操纵另一个类的对象,即一个类依赖于另一个类(应该尽可能地将相互依赖的类减至最少。如果类A不知道B的存在,它就不会关心B的任何改变(这意味着B的改变不会导致A产生任何bug),即尽可能减少类之间的耦合)。
  • 聚合(aggregation "has-a"):即类A的对象包含类B的对象(关联)。
  • 继承("is-a"):一般而言,如果类A扩展类B,类A不但包含从类B继承的方法,还会有一些额外的功能。

2.采用UML(Unified Modeling Language,统一建模语言)绘制类图,用来描述类之间的关系。类用矩形表示,类之间的关系用带有各种修饰的箭头表示。


预定义类

对象与对象变量

1.要想使用对象,就首先必须构造对象,并指定其初始状态。然后对对象应用方法。

2.在Java程序设计语言中,使用构造器(constructor,构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象

3.构造器的名字应该与类名相同。因此Date类的构造器名为Date,要想构造一个Date对象,需要在构造器前面加上new操作符。

4.对象变量并没有实际包含一个对象,它只是引用一个对象。(在Java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。new操作符的返回值也是一个引用)

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
import java.util.Date;

public class OopStudy {
public static void main(String[] args) {
/*Date类描述时间,便于控制格式和设计*/
/*该表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。*/
new Date();
/*将对象传递给方法*/
System.out.println(new Date());
/*对创建的对象应用方法*/
String date= new Date().toString();
System.out.println(date);
/*构造的对象多次使用需要将对象存放在一个变量中:*/
Date birthday = new Date();
System.out.println(birthday);

/*变量deadline不是一个对象,实际上也没有引用任何对象。此时,不能在这个变量上使用任何Date方法*/
Date deadline; /*可以引用Date类型的对象*/
/*String s = deadline.toString();*/

/*首先初始化变量deadline
1.可以引用一个新构造的对象;
2.引用一个已有的对象;
*/
//deadline = new Date();
//deadline=birthday; /*deadline birthday两个变量都引用同一个对象*/


}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Date;

public class OopStudy {
public static void main(String[] args) {
/*
1.表达式new Date()构造了一个Date类型的对象,它的值是对新创建对象的一个引用
2.该引用存储在变量deadline中
Date deadline = new Date();
*/
Date deadline;
/*可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象*/
deadline=null;

deadline= new Date();
if(deadline!=null)
System.out.println(deadline);

}
}

1.可将Java的对象变量看作类似于C++的对象指针。Java中的null引用对应C++中的NULL指针。

2.所有的Java对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。

3.在Java中,必须使用clone方法获得对象的完整副本。

1
Date birthday; //Java
1
2
3
Date* birthday;  //C++
Date* deadline = new Date(); //C++
/* 如果把一个变量复制到另一个变量,两个变量就指向同一个日期,即它们是同一个对象的指针 */

LocalDate类

1.Java类库分别包含了两个类:一个是用来表示时间点的Date类,另一个是使用日历表示法的LocalDate类。(时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个点就是纪元(epoch),它是UTC(Coordinated Universal Time)时间1970年1月1日00:00:00。Date类所提供的日期处理遵循了世界上大多数地区使用的Gregorian阳历表示法。但是并不适用中国或希伯来的阴历表示)

2.不要使用构造器来构造LocalDate类的对象。应当使用静态工厂方法(factory method),它会代表你调用构造器。

3.LocalDate类封装了实例域来维护所设置的日期。

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
import java.time.LocalDate;
import java.time.Month;

public class LocalDateStudy {
public static void main(String[] args) {
/*LocalDate.now() 构造一个新对象,表示构造这个对象时的日期*/
LocalDate date = LocalDate.now();
System.out.println(date);
/*可提供年、月和日来构造对应一个特定日期的对象*/
LocalDate time = LocalDate.of(2021, 9, 9);
System.out.println(time);
Print(time);
/*
1.plusDays方法会得到一个新的LocalDate,如果把应用这个方法的对象称为当前对象,
这个新日期对象则是距当前对象指定天数的一个新日期
2.plusDays方法会生成一个新的LocalDate对象,然后把这个新对象赋给变量。
原来的对象不做任何改动。即plusDays方法没有更改调用这个方法的对象
*/
LocalDate time2 = time.plusDays(1000);
Print(time2);

}

private static void Print(LocalDate time) {
/*获取对应年月日*/
int year = time.getYear();
Month mo = time.getMonth();
/*
System.out.println(mo.getValue());
System.out.println(mo.toString());
*/
int month = time.getMonthValue();
int day = time.getDayOfMonth();
System.out.println(year);
System.out.println(month);
System.out.println(day);
}
}

更改器方法与访问器

1.与LocalDate.plusDays方法不同,GregorianCalendar.add方法是一个更改器方法(mutator method),调用这个方法后,someDay对象的状态会改变。
2.只访问对象而不修改对象的方法有时称为访问器方法(accessor method)。如LocalDate.getYear和GregorianCalendar.get就是访问器方法。
3.在C++中,带有const后缀的方法是访问器方法,默认为更改器方法。但是,在Java语言中,访问器方法与更改器方法在语法上没有明显的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Calendar;
import java.util.GregorianCalendar;

public class MethodStudy {
public static void main(String[] args) {
GregorianCalendar someDay = new GregorianCalendar(2002,7,6);
/* 日期增加1000天 */
someDay.add(Calendar.DAY_OF_MONTH, 1000);
int day = someDay.get(Calendar.DAY_OF_MONTH);
int month = someDay.get(Calendar.MONTH)+1; //; //月份从0开始,+1即我们使用的月份
int year = someDay.get(Calendar.YEAR);
System.out.println(year+" "+month+" "+day+" ");

}

}
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
import java.time.DayOfWeek;
import java.time.LocalDate;

public class CalendarTest {
public static void main(String[] args) {
LocalDate date = LocalDate.now();

int month = date.getMonthValue();
int today = date.getDayOfMonth();
/* 生成当前日期之前或之后n天的日期 */
/* 设置为该月的第一天 当前天数-当前天数+1*/
System.out.println(today);
date = date.minusDays(today-1);
System.out.println(date.getDayOfMonth());
/* 得到当前日期是星期几 */
DayOfWeek weekday = date.getDayOfWeek();
int value = weekday.getValue();
/* 打印表头和缩进 */
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
for(int i=1;i<value;i++) {
System.out.print(" ");
}

while(date.getMonthValue()==month) {
System.out.printf("%3d",date.getDayOfMonth());
if(date.getDayOfMonth()==today) {
System.out.print("*");
}else {
System.out.print(" ");
}
date=date.plusDays(1);

if(date.getDayOfWeek().getValue()==1) {
System.out.println();
}

}
if(date.getDayOfWeek().getValue()!=1) {
System.out.println();
}

}
}

用户自定义类

简单类的定义

1
2
3
4
5
6
7
8
9
10
11
12
class ClassName
{
field1
field2
. . .
constructor1
constructor2
. . .
method1
method2
. . .
}

1.源文件名是EmployeeTest.java,这是因为文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class和Employee.class。(由此可知只要两个Java文件存在同名类(无论是否公有)会导致报错)

1
2
#将程序中包含main方法的类名提供给字节码解释器,以启动该程序
java EmployeeTest

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

import java.time.LocalDate;
public class EmplyeeTest{
public static void main(String[] args) {
/* fill the staff array with three Employee objects */
Employee[] staff= new Employee[3];

staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

for(Employee e:staff) {
e.raiseSalary(5);
System.out.println(e.toString());
}

for (Employee e : staff) {
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay="
+ e.getHireDay());
}
}
}
class Employee{
//instance fields
/* 1.在Employee类的实例中有3个实例域用来存放将要操作的数据 */
/* 2.关键字private确保只有Employee类自身的方法能够访问这些实例字段,而其他类的方法不能够读写这些字段*/
/* 3.可以用public标记实例字段(极为不提倡),public数据字段允许程序中的任何方法对其进行读取和修改,这完全破坏了封装。
* 任何类的任何方法都可以修改public字段,某些代码可能使用这种存取权限,而并非所希望的。
* 因此强烈建议将实例字段标记为private。
* 4.有两个实例字段本身就是对象:name字段是String类对象,hireDay字段是LocalDate类对象。这种情形十分常见,
* 类包含的实例字段通常属于某个类类型。
*/

private String name;
private double salary;
private LocalDate hireDay;
/* 该类的所有方法都被标记为public,关键词public意味着任何类的任何方法都可以调用这些方法(共有4种访问级别)*/
//constructor
public Employee(String n,double s,int year,int month,int day) {
name=n;
salary=s;
hireDay=LocalDate.of(year,month,day);

}
//method
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}

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

@Override
public String toString() {
// TODO Auto-generated method stub
return "Employee{name="+name+",salary="+salary+",hireday="+hireDay.toString()+"}";
}

}

多个源文件的使用

1.习惯上将每一个类存在一个单独的源文件中。如将Employee类存放在文件Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。

2.编译文件

  • 使用通配符调用Java编译器:所有与通配符匹配的源文件都将被编译成类文件。
1
javac Employee*.java
  • 当Java编译器发现EmployeeTest.java使用了Employee类时会查找名为Employee.class的文件。如果没有找到这个文件,就会自动地搜索Employee.java,然后,对它进行编译。更重要的是:如果Employee.java版本较已有的Employee.class文件版本更新,Java编译器就会自动地重新编译这个文件。
1
javac EmployeeTest.java

初识构造器

1
2
3
4
5
6
7
public Employee(String n,double s,int year,int month,int day) {
name=n;
salary=s;
hireDay=LocalDate.of(year,month,day);

}

1.构造器与类同名。在构造Employee类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态。(所有的Java对象都是在堆中构造的,构造器总是结合new操作符一起使用)

2.构造器总是结合new运算符来调用,不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有0个、1个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作符一起调用
1
2
3
4
5
6
7
new Employee("Carl Cracker", 75000, 1987, 12, 15);

/*
name="Carl Cracker";
salary=75000;
hireDay=LocalDate.of(1987, 12, 15);
*/

不要在构造器(所有方法)中定义与实例字段同名的局部变量。

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
public class ConstructorStudy {
public static void main(String[] args) {
Person person = new Person(1,"Tim");
System.out.println(person.toString());
/*
Person [id=1, name=null]
*/

}
}
class Person{
private int id;
private String name;
public Person(int i, String n) {
String name = n;
id = i;
/*
这个构造器声明了局部变量name。该变量只能在构造器内部访问,同时该变量会遮蔽(shadow)同名的实例字段。
*/
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + "]";
}
}

var声明局部变量

1.在Java10中,如果可以从变量的初始化值推导出它们的类型,那么可以使用var关键词声明局部变量,而无需指定类型。

2.如果无需了解Java API就可从等号右边看出类型,在该情况下都可使用var表示法。不过不会对数值类型使用var,避免关注0、0L和0.0的区别。

3.var关键字只能用于方法中的局部变量。参数和字段的类型必须声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class VarStudy {
public static void main(String[] args) {
Person person = new Person(12,"Lucy");
var man = new Person(32,"Kate"); //避免重复写类型名Person
System.out.println(person.toString()+" "+man.toString());
}
}

class Person{
private int id;
private String name;
public Person(int i, String n) {
name = n;
id = i;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + "]";
}
}

null引用

1.一个对象变量包含一个对象的引用,或者包含一个特殊值null(没有引用任何对象)。

2.对null值应用一个方法,会产生一个NullPointerException异常。

3.定义一个类时,需要知道那些字段可能为null(当要接受一个对象引用作为构造参数,明确是否可接受可有可无的值,若不是使用拒绝null参数的方法更为合适)。

1
2
LocalDate birthday = null;
System.out.println(birthday.toString()); //java.lang.NullPointerException because "birthday" is null
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
import java.time.LocalDate;
import java.util.Objects;

public class NullStudy {
public static void main(String[] args) {
var cat = new Cat(12,null,2012,5,8);
System.out.println(cat.toString());
}
}

class Cat{
private int age;
private String name;
private LocalDate birthday;
public Cat(int g, String n, int year,int month,int day) {
age = g;
/*age为基本类型,不可能为null birthday非null,因为它初始化为一个新的LocalDate对象,
但name可能为null,如果调用构造器为n提供的实参为null,name就是null。
1.
if(n==null) {name="unknown";}else{name=n;}
2.
name=Objects.requireNonNullElse(n,"unknown");
3.拒绝null参数(异常报告会提供该问题的描述,准确的指出问题所在的位置)
name=Objects.requireNonNull(n,"The name cannot be null");
*/
name = n;
birthday = LocalDate.of(year,month,day);
}
@Override
public String toString() {
return "cat [age=" + age + ", name=" + name + ", birthday=" + birthday+ "]";
}
}

隐式参数与显式参数

1.nextYear方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的Man类型的对象。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参数。(隐式参数也称为方法调用的目标或接收者。)

2.在每一个方法中,关键字this指示隐式参数。(可将实例字段与局部变量明显地区分开

3.在Java中,所有的方法都必须在类的内部定义。是否将某个方法设置为内联方法是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
public class Man {
private int age;
private String name;

public Man(int g, String n) {
age = g;
name = n;
}
/*方法用于操作对象以及存取它们的实例字段*/
/*man.age = man.age+year;*/
/*显式参数显式地列在方法声明中,例如int year。隐式参数没有出现在方法声明中。*/
public void nextYear(int year) {
age = age+ year;
}
/*
public void nextYear(int year) {
this.age = this.age+ year;
}
*/
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Man [age=" + age + ", name=" + name + "]";
}
}
1
2
3
4
5
6
7
8
public class Demo1 {
public static void main(String[] args) {
Man man = new Man(45,"Joe");
System.out.println(man.toString());
man.nextYear(12);
System.out.println(man.toString());
}
}

封装

1.需要获得或设置实例字段的值:

  • 一个私有的数据字段

  • 一个公共的字段访问器方法

  • 一个公共的字段更改器方法

优点:

  • 改变内部实现,除了该类的方法之外,不会影响其他代码。
  • 更改器方法可以执行错误检查

2.不要编写返回可变对象引用的访问器方法。

  • 如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象克隆是指存放在另一个新位置上的对象副本。(如果需要返回一个可变数据字段的副本,就应该使用clone)
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
public class Encapsulation {
public static void main(String[] args) {
People people = new People("Kate",2340);
System.out.println(people.getName());
people.setSalary(-23);
System.out.println(people.getSalary());

}
}
class People{
/*
private String firstName;
private String lastName;
*/
private String name;
private int salary;

public People(String na, int sa) {
this.name = na;
this.salary = sa;
}

/*1.访问器方法 由于只返回实例字段值,因此又称为字段访问器*/
/*2.name是一个只读字段。一旦在构造器中设置,就没有任何办法可以对它进行修改,
这样确保name字段不会受到外界的破坏*/
public String getName() {
return name;
//return firstName+" "+lastName;
}

public int getSalary() {
return salary;
}

/*salary不是只读字段,但是它只能用setSalary方法修改。*/
public void setSalary(int salary) {
/*错误检查*/
if(salary>=0) {
this.salary = salary; /* 前一个salary是该对象的salary实例字段,后一个salary为传递的参数*/
}
}
}
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
import java.util.Date;

public class Encapsulation {
public static void main(String[] args) {
/*
1.LocalDate类没有更改器方法,而Date类有一个更改器方法setTime,可以设置毫秒数。
Date对象是可变的,这一点破坏封装性。
*/
Employee harry = new Employee("harry",2012-1900,3-1,15);
System.out.println(harry.getHireDay().toString());
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10*365.25*24*60*60*1000;
d.setTime(d.getTime()-(long)tenYearsInMilliSeconds);
System.out.println(harry.getHireDay().toString());
/*
d和harry.hireDay引用同一个对象,
对d调用更改器方法就可以自动地改变这个Employee对象的私有状态
*/

}
}
class Employee{
private String name;
private Date hireDay;

public Employee(String name, int year,int month,int day) {
this.name = name;
this.hireDay = new Date(year,month,day);
}

public Date getHireDay() {
return hireDay;
/*return (Date) hireDay.clone();*/
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

基于类的访问权限

1.一个方法可以访问所属类的所有对象的私有数据。

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
import java.util.Date;
import java.util.Objects;

public class Encapsulation {
public static void main(String[] args) {
Employee boss = new Employee("Harry");
Employee harry = new Employee("Harry");
System.out.println(harry.equals(boss));

}
}
class Employee{
private String name;

public Employee(String name) {
this.name = name;
}

public boolean equals(Employee other) {
/*
该方法访问harry的私有字段,还访问了boss的私有字段。这是合法的,
其原因是boss是Employee类型的对象,而Employee类的方法可以访问任何Employee类型对象的私有字段。
*/
return name.equals(other.name);
}
}

私有方法

1.绝大多数方法都被设计为公有的,但在某些特殊情况下,将方法设计为私有的可能很有用。有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现关系非常紧密,或者需要一个特殊协议或者调用次序。最好将这样的方法设计为私有方法。

2.在Java中实现一个私有的方法,只需将关键字public改为private即可。

3.对于私有方法,如果改变了方法的实现方法,将没有义务保证该方法仍然可用。如果数据的表达发生了变化,这个方法可能会变得难以实现,或者不再需要。重点在于只要方法是私有的,类的设计者就可以确信它不会被外部的其他类操作调用,可以将其删去

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
public class PrivateDemo {
public static void main(String[] args) {
/*
* 虽然私有方法在外部类无法调用,但在仍可在其内部类中使用,
* 通过调用public方法进而间接实现外部调用,
* 但是违背了设计本意,不建议使用
*/
Test test = new Test();
System.out.println(test.add(1,2));
System.out.println(test.addOnePublic(1));
/*
System.out.println(test.addOne(1));
The method addOne(int) from the type Test is not visible
*/
}
}

class Test{
public int add(int a,int b) {
return a+b;
}
public int addOnePublic(int a) {
return addOne(a);
}
private int addOne(int a) {
return a+1;
}

}

final实例字段

1.可以将实例字段定义为final,该字段在构建对象时必须初始化。即必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。

2.final修饰符对于类型为基本类型或者不可变类的字段尤其有用(如果类中的所有方法都不会改变其对象,这种类就是不可变的类,如String类就是不可变的)。

3.对于可变的类,使用final修饰符可能会造成混乱。final关键字仅表示存储在变量中的对象引用不会再指示另一个不同的对象,但该对象可以更改。

1
2
3
4
5
6
7
8
9
public class FinalDemo {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
Man man = new Man(12,"Joe",stringBuilder);
System.out.println(man.toString());
man.addContext("Hello World");
System.out.println(man.toString());
}
}
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
public class Man {
private int age;
/*
* 如不使用构造器初始化final字段
* 会报The blank final field name may not have been initialized错误
*/
private final String name;
private final StringBuilder evaluations;

public Man(int a, String n,StringBuilder ev) {
this.age = a;
this.name = n;
this.evaluations =ev;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}

public StringBuilder getEvaluations() {
return evaluations;
}

public void addContext(String ct) {
evaluations.append(ct);
}

@Override
public String toString() {
return "Man [age=" + age + ", name=" + name + ", evaluations=" + evaluations + "]";
}
}

静态字段和静态方法

静态字段

1.如果将一个字段定义为static,每个类中只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StaticDemo {
public static void main(String[] args) {
User user1 = new User();
user1.setId();
System.out.println(user1.getId());
User user2 = new User();
user2.setId();
System.out.println(user2.getId());
User user3 = new User();
user3.setId();
System.out.println(user3.getId());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class User {
/*
* 每一个User对象都有一个自己的id字段,但这个类的所有实例将共享一个nextId字段。
* 即如果有1000个User类的对象,则有1000个实例字段id,分别对应每一个对象。
* 但是,只有一个静态字段nextId。
* 即使没有User对象,静态字段nextId也存在。它属于类,而不属于任何单个的对象。
*
*/
private static int nextId=1;
private int id;

public int getId() {
return id;
}
public void setId() {
this.id = nextId;
nextId++;
/*
user1.id=User.id;
User.id++;
*/
}
}

静态常量

1
2
3
4
5
public final class Math {
public static final double E = 2.7182818284590452354;
public static final double PI = 3.14159265358979323846;
...
}
1
System.out.println(Math.PI);

1.在程序中,可以采用Math.PI的形式访问该常量。
2.如果关键字static被省略,PI就变成了Math类的一个实例字段,即需要通过Math类的对象来访问PI,并且每一个Math对象都有它自己的一份PI副本。

1
2
/*System.out也为静态常量,虽然由于每个类对象都可以修改公共字段,所以最好不要有公共字段,但公共常量(即final字段)却没问题。out被声明为final,所以不允许将其再重新赋值为另一个打印流*/
public static final PrintStream out = null;

System类有一个setOut方法可以将System.out设置为不同的流。原因在于,setOut方法是一个原生方法,而不是用Java语言实现的。原生方法可以绕过Java语言的访问控制机制。这是一种特殊的解决方法,在自己编写程序时不应这样处理。


静态方法

使用静态(属于类且不属于任何类对象的变量和函数)方法:

  • 方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供(Math.pow)
  • 方法只需要访问类的静态字段(Employee.mainId)
1
2
3
4
5
6
7
8
public final class Math {
...
/*在运算时,不使用任何Math对象。换句话说,没有隐式的参数。*/
public static double pow(double a, double b) {
return StrictMath.pow(a, b); // default impl. delegates to StrictMath
}
...
}
1
System.out.println(Math.pow(2,4));

1.静态方法是不在对象上执行的方法。(可以认为静态方法是没有this参数的方法)

2.静态方法不能访问实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。

3.可以调用静态方法而不需要任何对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Demo {
public static void main(String[] args) {
System.out.println(Employee.mainId); /*提供类名调用该方法*/
Staff Ming = new Staff();
System.out.println(Ming.mainId);
}
}

class Employee{
final static int mainId=100;

public static int getMainid() {
return mainId;
}
}

class Staff{
final static int mainId=100;

public int getMainid() {
return mainId;
}

}

可以使用对象调用静态方法,不过通常该静态方法方法与具体的对象关系不大,所有建议使用类名而非对象来调用静态方法


工厂方法

类似LocalDate(LocalDate.now)和NumberFormat(LocalDate.of)的类使用静态工厂方法(factory method)来构造对象。

1
2
3
4
5
6
7
/*格式化和解析数字 Currency 货币*/
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x=0.1;
System.out.println(currencyFormatter.format(x));
System.out.println(percentFormatter.format(x));
System.out.println(percentFormatter instanceof DecimalFormat);

使用工厂方法:

  • 无法命名构造器,构造器的名字必须和类名相同。
  • 使用构造器时无法改变构造对象的类型。该工厂方法实际返回DecimalFormat类的对象,NumberFormat的子类。

main方法

1.main方法是一个静态方法。

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

3.每一个类可以有一个main方法,常用于对类进行单元测试的技巧。

4.A类中含有main方法,B类中使用了A类

  • java A : 独立测试A类
  • java B : A类的main方法永远不会执行

方法参数

1.按值调用(call by value)表示方法接收的是调用者提供的值。按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。方法可以修改按引用传递的的变量值,而不能修改传按值传递的变量值。

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

3.方法参数共有两种类型:

  • 基本数据类型
  • 对象引用

方法不可能修改一个基本数据类型的参数。但可以修改对象引用的参数。实现一个改变对象参数状态的方法是完全可行的,方法得到的是对象引用的副本,原来的对象引用及这个副本都引用同一个对象。

4.Java对对象引用是按值传递的

5.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
/*
1.x被初始化为percent值的一个副本(也就是10)。
2.x乘以3后等于30。但是percent仍然是10。
3.这个方法结束之后,参数变量x不再使用。
*/
double percent = 10;
tripleValue(percent);
System.out.println(percent);


/*
1.x被初始化为harry值的一个副本,这里是一个对象的引用。
2.raiseSalary方法应用于这个对象引用。x和harry同时引用的那个Employee对象的薪金提高了200%。
3.方法结束后,参数变量x不再使用。当然,对象变量harry继续引用那个薪金增至3倍的员工对象
该方法通过对象引用的副本修改所引用对象的状态
*/

Employee harry = new Employee(20);
tripleSalary(harry);
System.out.println(harry.getSalary());

/*
swap方法的参数x和y被初始化为两个对象引用的副本,这个方法交换的是这两个副本。在方法结束时参数变量x和y都被丢弃。原来的变量a和b仍然引用这个方法调用之前所引用的对象
*/
Employee a = new Employee("Alice",20);
Employee b = new Employee("Bob",30);
swap(a,b);
System.out.println(a.toString());
System.out.println(b.toString());
}

public static void tripleValue(double x) {
x=x*3;
}
public static void tripleSalary(Employee x) {
x.raiseSalary(200);
}

public static void swap(Employee x,Employee y) {
Employee temp = x;
x = y;
y = temp;
}


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
public class Employee {
private String name;
private double salary;

public Employee(double salary) {
this.salary = salary;
}

public Employee(String name, double salary) {
super();
this.name = name;
this.salary = salary;
}

public double getSalary() {
return salary;
}

public void raiseSalary(double increase) {
salary *= (1+increase/100);

}

@Override
public String toString() {
return "Employee [name=" + name + ", salary=" + salary + "]";
}

}


对象构造

1.重载(overloading)

1.如果多个方法有相同的名字、不同的参数,便出现了重载。编译器必须挑选出具体调用哪个方法,它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,就会产生编译时的错误(重载解析 overloading resolution)。

2.Java允许重载任何方法,而不只是构造器方法。因此要完整地描述一个方法,需要指定方法名以及参数类型(方法签名 signature)。

3.返回类型不是方法签名的一部分,即不能有两个名字相同、参数类型相同但不同返回类型的方法。

1
2
var message = new StringBuffer();
var todoList = new StringBuilder("Hello World");

2.默认字段初始化

1.如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。如果不明确地对字段进行初始化,就会影响程序代码的可读性。

2.字段与局部变量的主要不同点:方法中的局部变量必须明确地初始化。但是在类中,如果没有初始化类中的字段,将会被自动初始化为默认值(0、false或null)。

1
2
3
4
5
6
7
8
9
public class Employee {
private String name;
private double salary;
private boolean isMale;
@Override
public String toString() {
return "Employee [name=" + name + ", salary=" + salary + ", isMale=" + isMale + "]";
}
}
1
2
3
Employee harry = new Employee();
System.out.println(harry.toString());
/*Employee [name=null, salary=0.0, isMale=false]*/

3.无参数构造器

1.由无参数构造器创建对象时,对象的状态会设置为适当的默认值。

2.如果写一个类时没有编写构造器,就会提供一个无参数构造器。这个构造器将所有的实例字段设置为默认值。

3.如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。

4.仅当类没有任何构造器的时候,才会得到一个默认的无参数构造器。编写类时如果写了一个构造器,要想让这个类的用户能够通过以下调用构造实例:

1
new ClassName();

必须提供一个无参数的构造器。当然,如果希望所有字段被赋予默认值,只需要:

1
2
public ClassName(){
}
1
2
3
4
5
public Employee() {
name = " ";
salary = 0;
hireDay = LocalDate.now();
}

4.显式字段初始化

1.确保不管怎样调用构造器,每个实例字段都要被设置为一个有意义的初值,这是一种很好的设计习惯。

2.可以在类定义中直接为任何字段赋值,会在执行构造器之前先完成这个赋值操作。初始值不一定是常量值,可以调用方法对字段进行初始化。

1
2
3
4
5
public class Employee {
private String name="";
private double salary=0;
private LocalDate hireDay=LocalDate.now();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Employee {
private static int nextId;
private int id = assignId();

private static int assignId() {
int r = nextId;
nextId++;
return r;
}

@Override
public String toString() {
return "Employee [id=" + id + "]";
}
}

5.参数名

  • 可在每个参数前面加上一个前缀a
  • 如果将参数命名为salary,salary将指示这个参数,而不是实例字段(参数变量会遮蔽同名的实例字段)。但是可以采用this.salary的形式访问实例字段
1
2
3
4
public Employee(String aName, double aSalary) {
name = aName;
salary = aSalary;
}
1
2
3
4
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}

6.调用另一构造器

1.this指示一个方法的隐式参数。

2.如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器(前提是存在该构造器)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Employee {
private static int nextId;
private String name;
private double salary;
private int id = nextId;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
nextId ++;
}
public Employee(double s) {
/* calls Employee(String , double)*/
this("Employee #"+nextId, s);
}
}

7.初始化块

初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造这个类的对象,这些块就会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Employee {
private static int nextId;
private String name;
private double salary;
private int id;
/*
1.无论使用哪个构造器构造对象,id字段都会在对象初始化块中初始化。首先运行初始化块,然后才运行构造器的主体部分
2.虽然可以在初始化块中设置字段,即使这些字段在类的后面才定义,但为了避免循环定义,不允许读取在后面初始化的字段,建议总是将初始化块放在字段定义之后
*/
{
id = nextId;
nextId++;
}
public Employee(String n,double s) {
name = n;
salary = s;
}
public Employee() {
name = " ";
salary = 0;
}
}

调用构造器的具体处理步骤:

  1. 如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。

  2. 否则

    1. 所有数据字段初始化为其默认值(0、false或null)
    2. 按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块
  3. 执行构造器主体代码

1.初始化静态字段:

  • 提供初始化值
  • 使用静态的初始化块(代码放在一个块中并标记关键字static)

2.在类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值是0、false或null。所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Employee {
private static int nextId;
private String name;
private double salary;
private int id;
/*员工Id起始值赋予小于1000的随机整数*/
static{
var generator = new Random();
nextId = generator.nextInt(1000);
}
{
id= nextId;
nextId++;
}
}

有些面向对象的程序设计语言,如C++有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中,最常见的操作是回收分配给对象的存储空间。由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。当然,某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要:

  • 如果一个资源一旦使用完就需要立即关闭,应当提供一个close方法完成必要的清理工作。
  • 如果可以等到虚拟机退出,可以使用方法Runtime.addShutdownHook增加一个”关闭锁”, 在Java9中可以使用Cleaner类注册一个动作,当对象不可到达时(除了Cleaner还能访问,其他对象都无法访问这个对象),就会完成这个动作。