Java核心技术 读书笔记:
第四章 对象与类
对象的理解:
每个对象包含对用户公开的特定功能部分和隐藏的实现部分。从根本上来说,只要对象能够满足要求,就不必关心其功能到底如何实现!
此外,每个对象都保存着描述当前状况的信息——对象的状态。对象状态的改变必须通过调用方法实现(如果不经过调用就可以改变对象状态,只能说明破坏了封装性!)
对象引用:
对象的创建通过对象提前写好的构造函数(无参 有参),new+构造才能够创建出一个新的对象
Date s = new Date()
Date s = k
实际上,k和s引用的是同一对象(new的Date的对象),指向相同!
所有的Java对象都存储在堆中,当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针!
类:
三种关系:
- 依赖(uses-a)如Order类使用Account类,是因为Order对象需要访问Account对象查看信用状态!——我们应该尽可能减少相互依赖:减少类之间的耦合!
- 聚合(has-a)包含关系
- 继承(is-a)
公共类和非公共类(是否带public)
- 源文件名必须与public类的名字相匹配,在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
- 一般习惯将类单独命名为xxx.java
类中的public方法:
public意味着任何类的任何方法都可以调用这些方法(一共有4个级别,后面介绍)
构造器:
- 与类同名
- 可以有一个以上的构造器
- 构造器可以有任意数目的参数
- 构造器没有返回值
- 总是伴随new一起调用!
var声明局部变量:
Java10中,如果可以从变量的初始值推导出它们的类型,可用var关键字声明局部变量,无须指定类型。
Employee harry = new Employee("Harry",5000,1989,10,1);
var harry = new Employee("Harry",5000,1989,10,1);
两者等效
隐式参数和显式参数:
如:
public viod raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
number007.raiseSalary(5)
其结果是将number007.salary字段新增5%(设置为了一个新值)
raiseSalary有两个参数,其一是前面的Employee类的对象,第二则是括号中的参数
关键词this指示隐式参数,可以改写:(强烈推荐)
public viod raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
可以将实例字段和局部变量明显区分出来
警告!(初探对象封装性)
不要编写返回可变对象引用的访问器方法。
例如:
class Employee
{
private Date hireDay;
...
public Date getHireDay()
{
return hireDay //Bad
}
}
其中的Date类有更改器方法setTime,也就是说Date对象是可变的,这就破坏了封装性!
Employee harry = ...;
Date d = harry.getHireDay();
double tenYearsInNilliseconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long)tenYearMilliseconds);
d和harry.hareDay引用的是同一个对象,对d调用更改器方法就可以自动地改变这个Employee对象的私有状态!
如果要返回一个可变对象的引用,首先应该对它进行克隆!对象克隆指放在另一个新位置上的对象副本。
class Employee
{
private Date hireDay;
...
public Date getHireDay()
{
return (Date)hireDay.clone(0) //Bad
}
}
谈谈私有方法和公共方法
由于公共数据非常危险,应该将数据字段设置为私有的字段(很好理解),对于方法来说,尽管大部分都是公共的,但有些情况下用私有会更好:如,数据的表示发生了变化,这个方法可能会变得难以实现,或者不再需要,这并不重要,重要的是,只要它是私有方法,类的设计者就可以确信它不会在别处使用没所以可以将其删去,如果一个方法是公共的,就不能简单的删除,因为有可能在别处依赖!
final实例字段
-
一旦设置,以后就不再修改这个字段,如Employee类中的name字段设置为final,因为在对象构造后,值不会改变,即没有setName方法。
-
对于基本类型或者不可变类的字段尤其有用:
对于可变的类,可能混乱:
private final StringBuilder evaluations;
它在Employee中初始化为:
evaluations = new StringBuilder();
final关键字只是表示存储在evaluations变量中的对象引用不会再指向另一个不同的StringBuilder对象。不过这个对象依旧是可以更改的!!也就是地址不变而已
静态字段与静态方法
静态字段:属于类,不属于对象!例如,要给每个员工一个唯一的标识码,这里给Employee类添加一个实例字段id和一个静态字段nextId;
class Employee
{
private static int nextId = 1;
private int id;
...
}
public void setId()
{
id = nextId;
nextId++;
}
当新增员工时,其id是在整个员工nextId基础之上的!也就是说,这个nextId是公共调用的!
静态常量用的更多,比如Math类下的PI值
以下两种情况下可以使用静态方法:
- 方法不需要访问对象的状态,因为它需要的所有参数都通过显式参数提供,如Math.pow
- 方法只需要访问类的静态字,如Employee.getNextId
静态工厂方法
类似LocalDate和NumberFormat的类使用静态工厂方法来构造对象。
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总是按值调用的。方法得到的是所有参数值的一个副本。也就是说,方法不能修改传递给它的任何参数变量的内容。
double percent = 10;
harry.raiseSalary(percent);
无论方法如何实现,在这个方法调用后,percent值还是10。
但是对于对象引用则不同!
public static void tripleSalary(Employee x)
{
x.raiseSalary(200);
}
当调用
harry = new Employee(...);
tripleSalary(harry);
具体为:
- x初始化为harry值的一个副本,这里就是一个对象引用。
- raiseSalary方法应用于这个对象引用。x和salary同时引用的那个Employee对象的工资提高了200%。
- 方法结束后x不再使用,对象变量harry继续引用那个工资增至3倍的员工对象
总结Java方法参数
- 方法不能修改基本数据类型的参数
- 方法可以改变对象参数的状态
- 方法参数不能让一个对象参数引用一个新的对象
对象构造
重载——同方法、不同参数
默认字段初始化:如果构造器中没有显示地为字段设置初值,则会被自动的赋为默认值!数值为0、布尔值为false、对象引用为null
无参构造器:如果编写一个类没有无参构造,就会为你提供一个无参数的构造器,如果已经只定义了有参,再调无参则不合法。
参数名的定义:
习惯将参数名和实例字段保持一致,通过this来区分:
public Employee(String name,double salary)
{
this.name = name;
this.salary = salary
}
this的另一用法:
this除了可以指示一个方法的隐式参数外,还可以调用同一个类的另一个构造器
public Employee(double s)
{
this("Employee #" + nextId,s);
nextId ++;
}
当调用new Employee(6000),Employee(double)构造器会调用Employee(String,double)构造器。
初始化块:
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”**关系是继承的明显特征
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字段。
覆盖方法:
如果要返回经理的奖金
public double getSalary()
{
return salary + bonus //不成功
}
因为salary是父类的私有字段,子类Manager的getSalary方法不能直接访问到!
如果我们想调用父类Employee的getSalary方法,而不是当前类的这个方法,可以用super.getSalary()
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字段的。
public class Person {
private String name;
public Person(String name) { this.name = name; }
public String getName() { return name; }
}
public class Student extends Person {
private int id;
public Student(String name, int id) {
super(name);
this.id = id;
}
}
Student没有name字段,但它内部的Person对象有,而且还可以打出来看。
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对象。
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;
}
}
输出:
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调用构造器,必须是子类构造器的第一条语句
- 子类构造器如果没有显式地调用超类的构造器,将自动地调用超类的无参数构造器,所以必须要求父类有无参构造,否则报错
多态
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,像这种的,一个对象变量可以指示多种实际类型的现象称为多态,在运行时可以自动地选择适当的方法,称为动态绑定
例子:
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[i]; //Error
原因很清楚:不是所有的员工都是经理,如果赋值成功,m有可能引用了一个不是经理的Employee对象,而在后面有可能会调用m.setBonus,这就会发生错误。
警告:
Manager[] managers = new Manager[10];
Employee[] staff = managers; //OK
这样是没有问题的,因为manger[i]是一个Manager就一定是一个Employee!一定要切记:这里的staff和mangers引用的是同一个数组,就是一开始new的长度为10的数组!
staff[0] = new Employee("Harry");
如果这么去赋值,编译器是可以接受的!但是!!staff[0]和managers[0]是相同的引用,我们把一个普通的员工Harry擅自归入到经理行列(数组)里面去了!!后面如果调用manager[0].setBonus(1000)的时候,将会试图调用一个根本不存在的实例字段,进而搅乱相邻存储空间的内容
牢记:所有数组要牢记创建时候的元素类型,并负责监督仅将类型兼容的引用存储到数组中!例如,使用new managers[10]创建数组是一个经理数组如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常
方法调用
-
编译器查看对象的声明类型和方法名。
-
确定方法调用中提供的参数类型。
-
如果是private、static、final或者构造器,那么编译器将可以准确地知道应该调用哪个方法。——静态绑定;
动态绑定——如果调用的方法依赖于隐式参数的实际类型,则必须在运行的时候使用动态绑定。
强制类型转换
对于对象:
由于在员工列表中,一部分是纯员工,有一部分是经理(子类),在创建数组的时候申明的是Employee对象,而Employee对象无法读取到其Manager字段或方法等属性(多态),那么在实际用Manager这个对象的时候,要先强制转换成Manager类型:
Manager boss = (Manager)staff[0];
将其复原为Manager对象,以便于访问其额外的字段,如bonus奖金。当然,前提是0号确实是Manager,如果“谎报”,则会报错ClassCastException,为了确保不会谎报,可以先判断一下:
if (staff[0] instanceof Manager)
{
boss = (Manager)staff[1];
...
}
受保护字段protected
一般来说,声明为private私有,对其他类都是不可见的,即,子类不能访问超类的私有字段。不过有时候希望限制超类中的某个方法只允许子类访问,或者希望子类的方法访问超类的某个字段。
例如,将Employee中的hireDay字段设为protected,而不是private,则Manager方法就可以访问到这个字段。
注意:
- 要谨慎使用,如果你的代码被别的程序员访问了受保护字段,那么后期维护时候,修改自身类则会影响到别人!
- 受保护的方法更具有实际意义,表明子类得到了信任,可以正确的使用这个方法,而其他类则不行
泛型类数组列表
ArrayList是一个有类型参数的泛型类。尖括号里面填写保存的元素对象类型,如ArrayList<Employee>
声明一个保存Employee对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Employee>();
//或者
var staff = new ArrayList<Employee>();
也可以省略右边括号里面的类型参数
ArrayList<Employee> staff = new ArrayList<>();
对象包装器与自动装箱
每个基本类型都有与之对应的类Integer、Long、Float、Double、Short、Byte、Character、Boolean;
<>尖括号中的类型参数不允许是基本类型
由于每个值分别包装在对象中,所以ArrayList<Integer>
效率远远低于int[]
自动装箱:
var list = new ArrayList<Integer>()
list.add(3)
此时,进行了自动装箱过程:
list.add(Integer.valueOf(3))
自动拆箱:此时拿到的n应该是<Integer>
类型
int n = list.get(i)
转换成:
int n = list.get(i).intValue();