Java核心技术4&5章(对象、类、继承)读书笔记

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);

具体为:

  1. x初始化为harry值的一个副本,这里就是一个对象引用。
  2. raiseSalary方法应用于这个对象引用。x和salary同时引用的那个Employee对象的工资提高了200%。
  3. 方法结束后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异常

方法调用

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

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

  3. 如果是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();
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值