Java核心技术卷一 2. java对象与类

面向对象程序设计概述

面向对象程序设计(简称 OOP )是主流的程序设计范型,取代了早年的结构化过程化程序设计开发技术。Java 是完全面向对象的,必须熟悉 OOP 才能够编写 Java 程序。

面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。程序中的很多对象来自标准库,还有一些自定义的。在 OOP 中,不必关心对象的具体实现,只要能够满足用户的需求即可。

传统的结构化程序设计首先要确定如何操作数据,然后在决定如何组织数据,以便于数据操作。而 OOP 将数据放在第一位,然后在考虑操作数据的算法。

面向对象更加适用于解决规模较大的问题。比如,如果实现一个简单的 web 游览器可能需要大约 2000 个过程,这些过程需要对一组全局数据进行操作。采用面向对象的设计风格,可能只需要大约100个类、每个类平均包含20个方法,这更容易程序员掌握,也容易掌握 bug。对象的数据出错了,只需要在数据项的20个方法中查找错误,这比在2000个过程中查找容易的多。

类与封装

类(class)是构造对象的模版或蓝图。由类构造(construct)对象的过程称为创建类的实例(instance)。标准的 java 库提供了几千个类。

封装(encapsulation)是与对象有关的一个重要概念。形式上,封装将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域(instance field),操纵数据的过程称为方法(method)。对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是对象的当前状态(state)。无论何时,只要向对象发送一个信息,他的状态就有可能发生改变。

实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。

可以通过扩展一个类来建立另外一个新的类,这个过程称为继承,所有的类都源自于一个超类 Object。

对象

对象的三个主要特性:

  • 对象的行为——可对对象施加的操作或方法
  • 对象的状态——施加方法时,如何响应
  • 对象标识——辨别具有相同行为与状态的不同对象

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

对象保存着描述当前特征的信息,这是对象的状态,对象状态的改变必须通过调用方法实现。

对象的状态并不能完全描述一个对象。每个对象都有一个唯一的身份。作为类的一个实例,每个对象的标识永远是不同的,状态也尝尝存在着差异。

对象的关键特性在彼此之间项目影响着。例如,对象的状态影响它的行为(行为肯定会根据状态做出不同的操作)

识别类

面向对象程序设计,没有所谓的从顶部的 main 函数开始编写。首先从设计类开始,然后再往每个类中添加方法。

比如在订单处理系统中,有这样一些名词:

  • 项目
  • 订单
  • 送货地址
  • 付款
  • 账户

还有一些动词:

  • 添加
  • 发送
  • 取消
  • 支付

对应的名词是类的一个参数,动词应该是类的一个方法。

类之间的关系

类之间常见的关系:

  • 依赖 uses-a
  • 聚合 has-a
  • 继承 is-a

依赖(dependence):是一种最明显的、最常见的关系。如,A类使用B类因为A对象需要访问B对象查看状态。如果一个类的方法操纵另一个类的对象,就可以说一个类依赖于另一个类。尽可能将相互依赖的类减至最少,让类之间的耦合度最小。

聚合(aggregation):聚合关系意味着类A的对象包含类B的对象。整体与部分之间是可以分离的。

继承(inheritance):如果类A扩展类B,类A不但包含类B继承的方法,还会拥有 一个额外的功能。

UML符号:

继承      ---------|>
接口实现  - - - - - |>
依赖      - - - - - >
聚合      <>---------
关联      -----------
直接关联  ---------->

使用预定义类

并不是所有类都有面向对象特征,比如 Math 类,只需要知道方法名和参数,不需要知道了解它的具体过程,这正是封装的关键所在。Math 类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。

对象与对象变量

使用对象,首先构造对象,使用构造器构造新实例,然后可以对对象应用方法。

new Date();
new Date().toString();

通常,希望构造的对象可以多次使用,因此,需要将对象存放在一个变量中。此时,birthday 就是对象变量,对象变量初始化之后的值是对存储在另外一个地方的一个对象的引用

Date birthday = new Date();
birthday.toString();

未初始化的对象变量不能调用对象的方法,否则会编译错误;对象变量如果赋值为null则表明对象变量目前没有引用任何对象,此时调用对象的方法会产生运行错误

Date birthday;
birthday.toString();//编译错误

birthday = null;
birthday.toString();//运行错误

日历类

Date类的实例有一个状态,即特定的时间点。

时间是用距离一个固定时间点的毫秒数(可正可负),这个点成为纪元,UTC 的时间为1970年1月1日 00:00:00

有个日历表示法类LocalDate类,不要使用构造器来构造LocalDate类的对象。

LocalDate.now();//静态工厂方法,表示构造这个对象时的日期。
LocalDate.of(1999, 12, 31);//使用年月日来构造一个日期。
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);//保存在一个对象变量中。
int year = newYearsEve.getYear();//获取年
int month = newYearsEve.getMonthValue();//获取月
int day = newYearsEve.getDayOfMonth();//获取日
LocalDate aThousandDaysLater = new newYearsEve.plusDays(1000);//距离当前日期1000天的日期。

更改器方法与访问器方法

使用类的 get 方法可以返回对象的状态,使用类的 set 方法可以改变对象的状态。通常在更改器名前面加上前缀 set ,在访问器名前面加上前缀 get 。

用户自定义类

想创建一个完整的程序,因该将若干类组合在一起,其中只有一个类有 main 方法。

Employee 类

简单的 Employee 类:

class Employee{
    // instance fields
    private String name;
    private double salary;
    private Date hireDay;
    
    // constructor
    public Employee(String n, double s, int year, int month, int day){
        name = n;
        salary = s;
        GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
        hireDay = calendar.getTime();
    }
    
    // method
    public String getName(){
        return name;    
    }
    
}

实际用处,构造了一个类数组,并填入了三个雇员信息。

Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker",...);
staff[1] = new Employee("Harry Hacker",...);
staff[2] = new Employee("Tony Tester",...);

利用 raiseSalary 方法将每个雇员的薪水提高 5%

for (Employee e : staff){
    e.raiseSalary(5);
}

最后调用,get 方法,或者,toString 方法将每个雇员的信息打印出来。

for (Employee e : staff){
    System.out.println("name=" + e.getName()
        + ",salary=" + e.getSalary
        + ",hireDay=" + e.getHireDay());
    
    // e.toString();
}

多个源文件的使用

在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
程序员习惯于将每一个类存在一个单独的文件中。

此时,就会有多个文件,当我们执行一个程序时,在命令行只需要编译程序的入口。当程序的入口使用到其他类时首先会查找.class的文件。如果没有找到这个文件,就会自动搜索.java文件,对它进行编译。如果.java.class版本新,java 编译器会自动地重新编译这个文件。

构造器

我们可以在构造的同事给实例域初始化为所希望的状态。

构造器的注意点:

  • 构造器与类同名
  • 每个类可以有一个以上的构造器,用户不定义构造器,编译器会自动生成构造器
  • 构造器可以有0个、1个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着 new 操作一起调用

隐式参数与显式参数

方法可以操作实例域,对象变量调用类的方法存取它的实例域。
类的方法有两种参数:

  • 隐式参数:出现在方法名前的 Employee 类对象。
  • 显式参数:方面括号内定义声明的数值。
public void raiseSalary(boucle byPercent){
    double raise =  salary * byPercent / 100;
    salary += raise;
}

每一个方法中,关键字 this 表示隐式参数,带上 this 可以将实例域与局部变量明显的区分开来。
如,在构造函数中,利用 this 将局部参数赋值给同名的隐式参数。

// instance fields
private String name;
private double salary;
private Date hireDay;

// constructor
public Employee(String name, double salary, int year, int month, int day){
    this.name = name;
    this.salary = salary;
    GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
    this.hireDay = calendar.getTime();
}

封装的优点

get 方法只返回实例域的值,又称为域访问器。
为了防止破坏这个域值的捣乱者,因该提供下面三项内容:

  • 一个私有的数据域
  • 一个公有的域访问器方法
  • 一个私有的域更改器方法

封装的优点:

  • 可以改变内部实现,除了该方法之外,不会影响其他代码。
  • 更改器方法可以执行错误检察,直接对域进行赋值不会进行这些处理。

注意:不要编写返回引用可变对象的访问器方法,会破坏程序的封装性,如果需要返回一个可变对象的引用,因该首先对它进行克隆,对象 clone 是指存放在另一个位置上的对象的副本。

class Employee{
    public Date getHireDay(){
        return hireDay.clone();    
    }
}

基于类的访问权限

一个方法可以访问所属类的所有对象的私有数据。A类的方法可以访问A类的任何一个对象的私有域。

私有方法

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

在某些特殊情况下,可以将方法设计为私有。如一些辅助方法,它们只是用于自身的方法内使用,因该使用 private 的修饰符,这些辅助方法不应该成为公有接口的一部分。

如果未来这个类不需要这个私有方法,可以直接删去,如果是公有方法就不能随便删除,因为其他文件的代码有可能依赖它,如果是私有的我们完全不对单位外部对它的依赖。

final 实例域

实例域定义为 final ,每一个构造器执行之后,实例域的值被设置,并且后面的操作不能对它进行更改。

final 修饰符大都应用于基本类型域或不可变类的域(如 String)。

静态域和静态方法

main 方法都被标记为 static 修饰符。

静态域

如果一个类中定义了一个静态域和一个实例域,并且生成了1000个类的对象,则此时有1000个实例域。但是只有一个静态域,即使没有一个类对象生成,静态域也存在。

  • 即使没有一个类对象生成,静态域也存在
  • 静态域属于类,不属于任何对象
  • 静态域内的变量改变是永久的,不随着新对象的生成改变
  • 静态域可以被对象调用
class Employee{
    private static int nextId = 1;
    private int id;
}

静态常量

静态变量使用的比较少,但静态常量却使用的比较多,例如,Math 类中的 PI 。

好处:

  • 静态常量是常量不可修改
  • 静态常量是静态域属于类,不需要生成对象即可使用
  • 静态常量可以被对象调用
public class Math{
    public static final double PI = 3.14.....;
}

静态方法

静态方法是一种不能向对象实施操作的方法,也就是没有隐式的参数,可以认为静态方法是没有 this 参数的方法,但是静态方法可以访问静态域。

  • 静态方法不可以操作对象,没有隐式的参数
  • 静态方法可以访问静态域
  • 静态方法可以被对象调用,静态方法与对象之间没有任何关系,所有为了避免混乱,建议使用类名来调用静态方法。
public static int getNextId(){
    return nextId;
}

以下两种情况使用静态方法:

  • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供的。
  • 一个方法只需要访问类的静态域

工厂方法

一个类可以使用静态工厂方法来构造对象。

public final class LocalDate
        implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable {
    ...
    public static LocalDate now(Clock clock) {
        Objects.requireNonNull(clock, "clock");
        // inline to avoid creating object and Instant checks
        final Instant now = clock.instant();  // called once
        ZoneOffset offset = clock.getZone().getRules().getOffset(now);
        long epochSec = now.getEpochSecond() + offset.getTotalSeconds();  // overflow caught later
        long epochDay = Math.floorDiv(epochSec, SECONDS_PER_DAY);
        return LocalDate.ofEpochDay(epochDay);
    }
    
    public static LocalDate of(int year, Month month, int dayOfMonth) {
        YEAR.checkValidValue(year);
        Objects.requireNonNull(month, "month");
        DAY_OF_MONTH.checkValidValue(dayOfMonth);
        return create(year, month.getValue(), dayOfMonth);
    }
    
    ...
}

这样构造日期对象:

LocalDate.now();//静态工厂方法,表示构造这个对象时的日期。
LocalDate.of(1999, 12, 31);//使用年月日来构造一个日期。

方法参数

按值调用:方法接收的是调用者提供的值。

按引用调用:方法接收的是调用者提供的变量地址。

方法参数共有两个类型:基本数据类型、对象引用。

java 语言总是采用按值调用的程序设计语言。

方法参数分为两种类型:

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

基本数据类型

假设有一个方法可以将参数值增加3倍:

public static void tripleValue(double x){
    x = 3 * x;
}

double percent = 10;
tripleVlaue(percent);

调用这个方法之后,percent的值还是 10 。

执行过程:

  1. x 被初始化为 percent 的一个拷贝
  2. x 被乘以 3 后等于 30。percent 还是 10 。
  3. 方法结束 x 不再使用。

总结:一个方法不肯能修改一个基本数据类型的参数。

对象引用

以下代码可以将雇员的薪水提高两倍:

public static void tripleSalary(Employee x){
    x.raiseSalary(200);
}

Employee harry = new Employee(...);
tripleSalary(harry);

执行过程:

  1. x 被初始化为 harry 值的拷贝,这是对象的引用的拷贝。
  2. raiseSalary 方法应用对象的引用,x 和 harry 同时引用的对象的薪水提高了 200%。
  3. 方法结束后 x 不在使用。harry 指向的引用以改变。

总结:方法得到了对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。

java 对对象采用的也是按值传递

引用类型获取的不是对象的地址,得到的是对象引用的拷贝,对象引用及其他的拷贝引用同一个对象。

下面一个例子来说明 java 不是按引用传递:

private static void swap(Employee x, Employee y) {
	// 此时,x 指向 对象 a,y 指向对象 b

	// 赋值,改变了 x 与 y 引用的对象,但是 a 与 b 引用的对象不变。
	// 所以 java 都是按值传参的
	Employee temp = x;
	x = y;
	y = temp;
	// 此时,x 指向 对象 b,y 指向对象 a
	
	// 如果 x 与 y 使用 set 改变了引用对象的值, a 与 b 引用的对象也改变,因为他们引用同一个对象。
	x.setName("x"); // 改变了对象 b 的值
	y.setName("y"); // 改变了对象 a 的值
}

public static void main(String[] args) {
	Employee a = new Employee("a");
	Employee b = new Employee("b");
	//此时,a 指向对象a,b 指向对象 b
	swap(a,b);
	//此时,a 指向对象 a,b 指向对象 b
	
	System.out.println(a.getName());// y
	System.out.println(b.getName());// x
}

总结方法参数的使用:

  • 一个方法不能修改一个基本数据类型的参数
  • 一个方法可以改变一个对象的参数的状态
  • 一个方法不能让对象参数引用一个新的对象,改变的知识自身的引用。

对象构造

重载

如果多个方法有相同的名字不同的参数不同的参数类型或数量),便产生了重载。

编译器通过各个参数给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选相应的方法。

注意:实现重载返回值也可以不同。但只有返回值不同不是重载的要求,不能有两个名字相同、参数类型也相同却返回不同类型值的方法。

默认域初始化

如果构造器没有显式地给域赋予初值,那么就会被自动地赋予默认值(0、false、null)。这是一个不好的习惯,如果一个对象默认值为 null ,调用 get 方法会得到一个 null 引用,这不是我们所希望的结果。

无参数的构造函数

即默认的构造器,当没有其他构造器时,系统默认自动创建,这个构造器将所有的实例域设置为默认值。

显示域初始化

确保不管怎么调用构造器,每个实例域都有自己的初始值,可以在类定义中将一个值赋给任何域。

class Employee{
    private String name = "";
}

参数名

参数变量用同样的名字将实例域屏蔽起来,可以使用 this 指定隐式参数。

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

调用另一个构造器

关键字 this 可以引用方法的隐式参数,还有另一个含义,如果构造函数的第一句是this(...),这个构造器将调用同一个类的另一个构造器。

public Employee(double s){
    this(name, s);
    name++;
}

采用这种方法使用 this 关键字非常有用,这样对公共的构造器代码部分只编写一次即可。

初始化块

三种初始化数据域的方法:

  • 在构造器中设置值
  • 在声明中赋值
  • 在初始化块中设置值

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

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(){
        name = "";
        salary = 0;
    }
}

这种机制不是必须的,也不常见,通常都是将初始化代码放到构造器中。

在Java中,有两种初始化块:静态初始化块和非静态初始化块
静态初始化块:使用static定义,当类装载到系统时执行一次。若在静态初始化块中想初始化变量,那仅能初始化静态类变量,即static修饰的数据成员。
非静态初始化块:在每个对象生成时都会被执行一次,可以初始化类的实例变量。

生成对象时,执行的顺序:

  1. 所有数据域初始化为默认值
  2. 只在第一次生成对象时,调用静态初始化块(只能初始化静态变量)
  3. 调用非静态初始化块(都能初始化)
  4. 调用构造函数主体代码

存在继承关系时,父类与子类的执行顺序:

先初始化父类的静态代码 ---> 初始化子类的静态代码 --->

初始化父类的非静态代码 ---> 初始化父类构造函数 --->

初始化子类非静态代码 ---> 初始化子类构造函数

对象析构与 finalize 方法

C++有显式的析构器方法,放置一些不再使用时需要执行的清理代码。

java有自动的垃圾回收器,不需要人工回收内存,所以 java 不支持析构器。

可以给java的类添加 finalize 方法,他会在垃圾回收器清除对象之前调用。

java 允许使用包将类组织起来,便于组织代码,分开管理,使用包的主要原因是确保类名的唯一性。

可以使用嵌套层次组织包,所有标准的 Java 包都处于 java 和 javax 包层次中。

常用的 java 类库的包:

  • java.lang -- 语言包:Java语言的基础类,包括Object类、Thread类、String、Math、System、Runtime、Class、Exception、Process等;

  • java.io -- 输入输出包:提供与流相关的各种包;

  • java.awt -- 抽象窗口工具包:Java的GUI类库,一般网络开发用不上

  • java.util -- 实用工具包:Scanner、Date、Calendar、LinkedList、Hashtable、Stack、TreeSet等;

  • java.net -- 网络功能包:URL、Socket、ServerSocket等;

  • java.sql -- 数据库连接包:实现JDBC的类库;

  • java.text -- 文本包:Format、DataFormat等。

类的导入

一个类可以使用所属包中的所有类,以及其他包的公有类。
访问别的包公有类的方法:

java.util.Date today = new java.util.Date();

import java.util.Date;
Date today = new Date();

使用*可以导入一个包中的所有类

import java.util.*

但是不能*导入以java为前缀的包,如:

import java.*;
import java.*.*;

如果两个包中有相同类名,会发生冲突,导致出错,如,util与sql都有Date类:

import java.util.*;
import java.sql.*;
Date today;// ERROR java.util.Date or java.sql.Date ?

//可以增加一个特性的类解决
import java.util.*;
import java.sql.*;
import java.util.Date;
Date today;

//如果都需要用到就要加上特定的类名了
java.util.Date deadline = new java.util.Date();
java.sql.Date today = new java.sql.Date(...);

静态导入

import 还可以导入静态方法静态域
如在开通增加一条指令:

import static java.lang.Math.*;

就可以直接使用 Math 的静态域和静态方法,如

sqrt(pow(x, 2) + pow(y, 2))

讲类放入包中

直接看代码:

package com.xul.javaPrimary;

此时类文件放入了对应的包里,默认情况下,类放在没有名字的默认包里(defaulf package)。

包作用域

标记为 public 的部分可以被任意的类使用;
标记为 private 的部分只能被定义他们的类使用;
没有定义修饰符的可以被同一个包中的所有方法访问。

类路径

Java 类路径告诉 java 解释器和 javac 编译器去哪里找它们要执行或导入的类。类(您可能注意到的那些 *.class 文件)可以存储在目录或 jar 文件中,或者存储在两者的组合中,但是只有在它们位于类路径中的某个地方时,Java 编译器或解释器才可以找到它们。

设置类路径

java -classpath /home/user/classdir:.:/home/user/archives/archive.jar MyProg
java -classpath c:\classdir;.;c:\archives\archive.jar MyProg

文档注释

JDK 的工具 javadoc,可以由源文件生成一个 HTML 文档。

注释的插入

javadoc 实用程序从下面几个特性中抽取信息:

  • 公有类与接口
  • 公有的和受保护的构造器及方法
  • 公有的和受保护的域

注释以/**开始,并以*/结束。

文档后面紧跟自由格式文本,标记由@开始。

第一句通常是概要性的句子,可以通过 HTML 标签修饰

类注释

类注释必须要 import 语句之后,类定义之前。

/**
 * adfasdfasdfasdfasdf
 * adfsdfasfsdafsadfsdaf
 */
public class Card{
    ...
}

方法注释

方法注释必须放在所描述的方法之前。除了通用标记外,还可以使用下面标记:

  • @param 变量描述
  • @return 描述
  • @throws 类描述

域注释

只需要对公有域(通常为静态常量)建立文档。

通用注释

可以在类文档中的标记:

  • @author 姓名 对应一名作者
  • @version 文本 当前版本的描述

可以用于所有文档中的标记:

  • @since 文本 对引入特性的版本描述

  • @deprecated 文本 标记对类、方法或变量添加一个不再使用的注释,应给出取代建议。

    @deprecated Use <code> setVisible(true) </code> instead

@see 和 @link 标记,增加超链接

包与注释概述

可以直接将类、方法和变量的注释放到源文件中就可以了。想要产生包注释,就需要在每一个包目录中添加一个单独的文件。

  1. 提供一个以 package.html 命名的 HTML 文件。在标记<body></body>之间所有文本都会被抽取出来。
  2. 提供一个以 package-info.java 命名的 JAVA 文件。文件必须包含/** .. */的 javadoc 注释,跟随在一个包语句之后。

还可以为所有的源文件添加概述性的注释。放置在名为 overview.html 文件中。位于包含所有文件的父目录中,在标记<body></body>之间所有文本都会被抽取出来。用户从导航栏中点击 Overview 时就会被显示。

注释的抽取

假设抽取的文件放到 docDirectory 下:

  1. 切换到包含要生成文档的源文件目录。

  2. 如果是一个包

    javadoc -d docDirectory nameOfPackage

    如果文件在默认包中

    javadoc -d docDirectory *.java

类设计技巧

让类更有 OOP 专业水准:

  1. 一定要保证数据私有
  2. 一定要对数据初始化
  3. 不要在类中使用过多的基本类型,使用其他类代替多个相关的基本类型的使用
  4. 不是所有的域都需要独立的域访问器和域更改器
  5. 将职责过多的类进行分解
  6. 类名和方法名要能够体现他们的职责
posted @ 2018-06-02 23:17  xuhongliang  阅读(182)  评论(0编辑  收藏  举报