《Java核心技术 卷一》读书笔记 第四章

   类:是构造对象的模板或蓝图。我们可以把将类想象成制作小甜饼的模具,将对象想象成小甜饼。由类构造对象的过程称为创建类的实例

   封装(数据隐藏):将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式(比如调用Math.random,不必了解如何具体实现)。对象中的数据称为实例字段(比如String name等),操作数据的过程称为方法。实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段

对象的三个主要特性

      行为:可以对对象完成哪些操作,或者可以对对象应用哪些方法。同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。

      状态:描述当前状况的信息。对象状态的改变必须通过调用方法实现。

      标识:每个对象都有一个唯一的标识,用于区分具有相同行为与状态的不同对象(比如在订单系统中,即使两个订单货物完全相同,那也是不同的订单)。

类之间的关系

      依赖(uses-a)如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将相互依赖的类减至最少。

      聚合(has-a)类A的对象包含类B的对象。

      继承(is-a)如果类A继承类B,那么类A不但包含从类B继承的方法,还会有一些额外功能。(比如有一个RushOrder类,继承自Order类,包含了一些用于优先处理的特殊方法,而其他的一般性方法都继承自Order类)

构造对象

     在Java中,需要使用构造器构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。构造器的名字与类名相同(比如Date类的构造器名为Date),要想构造一个Date对象,需要在构造器前加上new操作符。如果没有使用new操作符,只是 Date birthday; 的话,那么birthday并不是一个对象,而且实际上它也没有引用任何对象,此时不能使用任何Date方法

  通常构造的对象需要多次使用,因此需要将对象存放在一个变量中:

Date birthday = new Date();

  birthday就是对象变量,而Date就是对象。 

  也可以对刚刚创建的对象应用一个方法。Date类有一个toString方法——

String s = new Date().toString();

   如果是以下这种写法:

Date birthday = new Date();
Date deadline;
deadline = birthday;

   那么这两个变量都引用同一个对象。对象变量并没有实际包含一个对象,它只是引用一个对象。等同于C++中的 Date* birthday; 

LocalDate类

    标准Java类库分别包含了两个类:一个是用来表示时间点的Date类,另一个是用大家熟悉的日历表示法表示日期的LocalDate类。不要使用构造器来构造LocalDate类的对象,应当使用静态工厂的方法。

public class HelloJava {
	public static void main(String[] args) {
		LocalDate date = LocalDate.of(2021, 8, 22);//用of方法构造特定日期
        LocalDate date2 = LocalDate.now();//用now方法获取现在的日期
		int year = date.getYear();//获取年
		int month = date.getMonthValue();//获取月
		int day = date.getDayOfMonth();//获取日
		System.out.println(year+" "+month+" "+day);
	}
}

 注:Date类的获取年月日的方法getYear,getMonth和getDay已经废弃。

更改器方法和访问器方法

     访问对象并且修改对象的方法称为更改器方法。比如GregorianCalendar.add方法,调用这个方法后,对象的状态会改变。而访问对象但是不修改对象的方法称为访问其方法,比如plusDays方法,它会生成一个新的LocalDate对象,然后把修改后的内容赋给新对象,原来的对象不做改动。

  更改器方法: 

public class HelloJava {
	public static void main(String[] args) {//对象被修改了
		GregorianCalendar someDay = new GregorianCalendar(1999,11,31);
		someDay.add(Calendar.DAY_OF_MONTH, 1000);//增加一千天
		int year = someDay.get(Calendar.YEAR);
		System.out.println(year);
	}
}

  访问器方法: 

public class HelloJava {
	public static void main(String[] args) {//对象没有被更改
		LocalDate date1 = LocalDate.now();
		LocalDate date2 = date1.plusDays(1000);//修改后的状态赋给了一个新类
		int year1 = date1.getYear();
		int year2 = date2.getYear();
		System.out.println(year1);
		System.out.println(year2);
	}
}

    在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。

    在Java10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,而无需指定类型。 注意var关键字只能用于方法中的局部变量。参数和字段的类型必须声明。

隐式参数和显示参数

number007.raiseSalary(5);

    对于raiseSalary方法,一共有两个参数。第一个参数称为隐式参数。是出现在方法名前的对象(number007),有人把隐式参数称为方法调用的目标或接收者。第二个参数是位于方法名后面括号中的数值(5),这是一个显式参数。

     在每一个方法中,关键字this指示隐式参数。

     类的方法可以访问任何同一个类型对象的私有字段。假如worker和boss都是Employee类型的对象,那么worker的方法也可以访问boss的私有字段,比如名字等。

    在Java中,所有的方法必须在类的内部定义,但并不表示它们是内联方法。但是在C++中,通常在类的外面定义方法:

void Employee::raiseSalary(double byPercent)
{
     ...
}

  

字段访问器

     一些只返回实例字段值的方法,称为字段访问器

public String getName()
{
    return name;
}

  注意不要编写返回对象引用的访问方法,因为对象引用是可变的,这一点破坏了封装性。

private Date HireDay;
...
public Date getHireDay(){
    return HireDay;
}

  如果需要返回一个可变对象的引用,首先应该对它进行克隆。对象克隆是指存放在另一个新位置上的对象副本。

final实例字段

    可以将实例字段定义为final。这样的字段必须在构造对象时初始化,并且以后不能再修改这个字段。比如下面的代码表示name就是final实例字段,这个值初始化后不会再变,也没有setName方法。

private final String name;

  final修饰符对于类型为基本类型或者不可变类的字段尤其有用(如果类中的所有方法都不会改变其对象,这样的类就是不可变的类,比如String)。而对于可变的类如StringBuilder,使用final修饰符可能会造成混乱。

静态字段

     可以将一个字段定义为static,每个类只有一个这样的字段这个类的所有实例将共享一个静态字段。也就是说,静态字段属于类,而不属于任何单个的对象。

class Employee{
	public Employee(int id) {
		this.id = id;
	}
	
	public void addId(){
		this.nextId++;
	}
	public void showId() {
		System.out.println("id为:"+ this.id + " " + "nextId为:" + this.nextId);
	}
	private static int nextId = 1;
	private int id;
}
public class HelloJava {
	public static void main(String[] args) {
		Employee one = new Employee(1);
		Employee two = new Employee(99);
		one.showId();
		two.showId();
		one.addId();
		one.showId();
		two.showId();
	}
}

 可以发现,one和two两个Employee类对象,它们的Id是不同的,但是nextId却相同,并且对一个对象的nextId做出修改,另外一个对象的nextId会保持同步。

静态常量

       可以用类名来调用静态常量,不需要声明对象,比如Math类里的PI,调用的话直接用Math.PI即可。

public static final double PI = 3.1459265358979323846;

  虽然类中最好不要有公共字段,但因为是final,所以没有问题。

静态方法

       可以认为静态方法就是没有隐式参数的方法,比如Math类的pow方法,它调用时没有使用任何Math对象——Math.pow(x,a),也就是直接用类名就可以调用了

        静态方法不能访问实例字段,但是可以访问静态字段。比如上面的Employee类

	public static int getId() {
		return id;
	}

  这个方法会报错,因为id是实例字段,静态方法是不能访问的。

	public static int getNextId() {
		return nextId;
	}

  而这个方法就不会报错,因为nextId是静态字段。当然也可以去掉static,但是这样做的话就不能通过类名来调用这个方法了(Employee.getNextId),而是需要创造一个Employee对象,用这个对象来访问。

   总结:有两种情况可以使用静态方法——1.方法不需要访问对象状态(不需要隐式参数),因为它所需要的所有参数都通过显式参数提供。  2.方法只需要访问类的静态字段。

   因为main方法不需要对任何对象进行操作,因此也是一个静态方法。

工厂方法

      静态方法还有另外一种用途,就是类似LocalDate和NumberFormat的类使用静态工厂的方法来构造对象。

NumberFormat percent = NumberFormat.getPercentInstance();

 不能使用构造器的原因——1.无法命名构造器,构造器的名字必须与类名相同,但是很多时候需要用不同方法去构造(比如LocalDate的now方法和of方法) 2.使用构造器时,无法改变所构造对象的类型。工厂方法可以返回各种子类,构造器只能返回大类

按值调用与按引用调用

        按值调用表示方法接收的是调用者提供的值按引用调用表示方法接收的是调用者提供的变量地址。 方法可以修改按引用传递变量的值,而不能修改按值传递变量的值(其实就是C++的形参与实参的关系,引用就是加上了&)。

       按值调用举例:假如在Employee类中加入一个方法——

	public void add(int x) {
		x += 10;
	}

      然后在主函数中调用,分析一下过程:1.x初始化为num的一个副本(形参,也就是10) 2.x加上10后等于20,但是num仍为10。  3.方法结束后,参数变量x不再使用。

public static void main(String[] args) {
		int num = 10;
		Employee a = new Employee(10);
		System.out.println("调用add函数前为:"+num);
		a.add(10);//这个就是按值调用
		System.out.print("调用add函数后为:"+num);
	}

    按引用调用举例:给Employee类增加另外一个方法,这时传入一个类。

	public void add(Employee one) {
		one.id += 10;
	}

   在主函数进行调用——1.one初始化为a的一个副本,这里就是一个对象引用。 2.add方法应用于这个对象引用。one和a同时引用一个Employee对象。  3.方法结束后,参数变量one不再使用,但是a做出了修改。

public static void main(String[] args) {
		Employee a = new Employee(10);
		System.out.print("调用add函数前——");
		a.showId();
		a.add(a);
		System.out.print("调用add函数后——");
		a.showId();
	}

 但是要注意一点,Java对对象采用的并不是引用调用,而是按值传递的。

public static void swap(Employee x,Employee y)
{
    Employee temp = x;
    x = y;//这个就触犯了 不能让一个对象参数引用一个新的对象 
    y = temp;
}

   实际上并不能交换传入的对象。  总结一下Java中对方法参数能做什么和不能做什么——1.方法不能修改基本数据类型(数值型,布尔型等)。  2.方法可以改变对象参数的状态(传入一个类修改该类的各项数值)。  3.方法不能让一个对象参数引用一个新的对象。(比如交换两个对象)

重载

     如果有多个方法有相同的名字,不同的参数,便出现了重载。要完整描述一个方法,需要指定方法名以及参数类型,这叫作方法的签名。返回类型不是方法签名的一部分。也就是说,不能有两个名字相同,参数类型也相同却有不同返回类型的方法。

     根据重载的定义,也就有了重载构造器——名字相同,参数不同的构造器。

   

 字段的默认值

      字段的默认值为——数值为0,布尔值为false,对象引用为null。如果写一个类的时候没有编写构造器,那么就会提供一个无参数构造器。这个构造器将所有实例字段设置为默认值。

      当然我们也可以自己写构造器(可以有参也可以无参)

public Employee(int beginSalary) {
	name = "";
	salary = beginSalary;
	hireDay = LocalDate.now();
}

字段的初始化

   可以直接在声明时赋值(实例字段初始化),也可以通过调用方法,通过返回值赋值(显式字段初始化。还有一个不太常见的初始化块方法。

	private String name="";//实例字段初始化,直接赋值
class Employee
{
    private static int nextId;
    private int id = assignId();///显式字段初始化
    private static int assignId()
    {
        int r = nextId;
        nextId++;
        return r;     
    }
}

 参数名

   参数命名一般有两种方法——1.与字段名相同,只不过前面加一个"a"。 2.用this

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

  (注意Employee这个类只是用于演示,并不是一成不变的)

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

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

        2.否则,(a)所有数据字段初始化为默认值  (b)按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块。

        3.执行构造器主体代码。

 初始化块(对象初始化块)

 在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。

	{
		id = nextId;
		nextId++;
	}

  静态初始化块

  如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块。

	static {
		var generator = new Random();
		nextId = generator.nextInt(10000);//产生0到10000的随机数
	}

  this调用另一个构造器

   如果构造器的第一个句子是this(...),this里的参数符合另外一个构造器,那么另外一个构造器就会被调用。

	public Employee(String n,double s)
	{
		name = n;
		salary = s;
	}
	
	public Employee(double s) {
		this("Employee #" + nextId, s);//调用上面的构造器
	}

  无参数构造器

  先把类内的字段赋为默认值,然后执行初始化块。

	public Employee() {
		// id根据初始化块进行初始化
        // name赋为""
	}

   详情参见P132示例

  

       包将类组织在一个集合中。 

包名

    为了保证包名的绝对唯一性,要用一个因特网域名以逆序的形式作为包名,然后可以追加一个工程名。举例:考虑域名horstmann.com,对应的包名就是com.horstmann,然后追加一个工程名比如com.horstmann.corejava。 如果把Employee类放在这个包里,那么这个类的完全限定名就是com.horstmann.corejava.Employee。

访问权限

    一个类可以使用所属包中的所有类,以及其他包中的公共类(需要使用import导入包)。

    对于类,方法,变量来说,标记为public的部分可以由任意类使用,标记为private的部分只能由定义它们的类使用。如果没有指定public或private,则可以被同一个包中的所有方法访问。

类的导入

    有两种方式访问另一个包中的公共类——1.使用完全限定名(包名后面跟着类名)。 2.使用import语句。

java.time.LocalDate today = java.time.LocalDate.now();//第一种完全限定名方法

   一旦使用import语句,就不用写出类的全名了。import语句应该位于源文件的顶部(但位于package语句的后面)

import java.time.*;
...
LocalDate today = LocalDate.now();

  当然也可以导入一个包中的特定类。

import java.time.LocalDate;

  但是需要注意的是,不能使用 import java.* 或 import java.*.* 导入以java为前缀的所有包。

 

  在发生命名冲突时(比如java.util和java.sql包都有Date类,并且两个类都import了),那么需要增加一个特定的import语句。

import java.util.*;
import java.sql.*;
import java.util.Date;//增加这个特殊语句,表明Date类属于util包

  如果这两个Date类都需要使用,那么调用类时,需要在每个类名的前面加上完整的包名(完全限定名)。

var deadline = new java.util.Date();

  可以认为Java中的package和import语句类似于C++中的namespace和using指令。

静态导入

       import语句还允许导入静态方法和静态字段,而不只是类。

import static java.lang.System.*;//注意必须加static关键字
public class Java4_6 {
    public static void main(String[] args) {
		out.print("Hello!");
		exit(0);
	}
}

  要想将类放入包中,就必须将包的名字放在源文件的开头。如果没有在源文件中放置package语句,这个源文件中的类就属于无名包。

举例:

    在两个不同的包中,在com.horstmann.corejava这个包中定义了Employee类。Employee类就需要使用package语句。然后希望在Java4_6这个类中使用Employee类,就需要使用import语句。

 Employee.java中: 

package com.horstmann.corejava;

  Java4_6.java中: 

import com.horstmann.corejava.*;

public class Java4_6 {
    public static void main(String[] args) {
		Employee e= new Employee();
	}
}

  

 类的设计技巧

    1.一定要保证数据私有。2.一定要对数据进行初始化(最好不要依赖于系统的默认值)。 3.不要在类中使用过多的基本类型。 4.不是所有的字段都需要单独的字段访问器和字段更改器。5.分解有过多职责的类。 6.类名和方法名要能够体现它们的职责。 7.优先使用不可变的类。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值