Java笔记(二)
4.对象与类
4.1面向对象程序设计概述
面向对象程序设计(object-oriented programming,OOP)
添加内容:
特征:
- 封装性:内部的操作对外部而言不可见,当内部的操作都不可直接使用的时候才是安全的。
- 继承性:在已有结构的基础上继续进行功能的扩充。
- 多态性:是在继承性的基础上扩充而来的概念,指的是类型的转换处理。
面向对象程序开发的步骤:
- OOA:面向对象分析
- OOD:面向对象设计
- OOP:面向对象编程
4.1.1 类
- 类(class)是构造对象的模板或蓝图。
- 由类构造(construct)对象的过程称为创建类的实例(instance)。
- 封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。
- 对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。
4.1.2 对象
- 对象的行为(behavior)
- 对象的状态(state)
- 对象的标识(identity)
4.1.3 识别类
4.1.4 类之间的关系
在类之间,最常见的关系有:
- 依赖(“use-a”):如果一个类的方法使用或操纵另一个类的对象,那么一个类依赖于另一个类。应该尽可能地将相互依赖的类减至最少。
- 聚合(“has-a”):包容关系意味着类A的对象包含类B的对象。
- 继承(“is-a”)
用UML(Unified Modeling Language,统一建模语言)绘制类图。
4.2 使用预定义类
4.2.1 对象与对象变量
要想使用对象,首先必须构造对象,并指定其初始状态。然后对对象应用方法。
在Java中,使用构造器(constructor,或称构造函数)构造新实例。构造器的名字应该与类名相同。
在Java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用。new操作符的返回值也是一个引用。
4.2.2 Java类库中的LocalDate类
4.3 用户自定义类
文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
关于构造器:
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作符一起调用,所有的Java对象都是在堆中构造的。
4.3.5 用var声明局部变量
在Java10中,如果可以从变量的初始值推导出它们的类型,那么可以用var关键字声明局部变量,而无须指定类型。
Employee harry = new Employee("Harry Hacker",50000,1989,10,1);
//只需要写以下代码,这样可以避免重复写类型名Employee
var harry = new Employee("Harry Hacker",50000,1989,10,1);
注意:var关键字只能用于方法中的局部变量。参数和字段的类型必须声明。
4.3.6 使用null引用
一个对象变量包含一个对象的引用,或者包含一个特殊值null,后者表示没有引用任何对象。
如果对null值应用一个方法,会产生一个NullPointerException异常。正常情况下,程序并不捕获这些异常。
LocalDate birthday = null;
String s = birthday.toString(); //NullPointerException
4.3.7 隐式参数与显式参数
方法用于操作对象以及存取它们的实例字段。
4.3.8 封装的优点
getXXX方法是访问器方法,由于只返回实例字段值,因此又称为字段访问器。
获取或设置实例字段的值,需要提供:
- 一个私有的数据字段
- 一个公共的字段访问器方法
- 一个公共的字段更改器方法
这样比提供一个简单的公共数据字段复杂些,但有下列明显的好处:
- 可以改变内部实现,而除了该类的方法之外,这不会影响其他代码。
- 更改器方法可以完成错误检查,而只对字段赋值的代码可能没有这个麻烦。
如果需要返回一个可变对象的引用,首先应该对它进行克隆(clone)。对象克隆是指存放在另一个新位置上的对象副本。
class Employee{
private Date hireDay;
...
public Date getHireDay(){
return hireDay;//BAD
}
...
}
class Employee{
private Date hireDay;
...
public Date getHireDay(){
return (Date) hireDay.clone();//BAD
}
...
}
4.3.9 基于类的访问权限
方法可以访问调用这个方法的对象的私有数据。一个方法可以访问所属类的所有对象的私有数据。
4.3.10 私有方法
在Java中,要实现私有方法,只需将关键字public改为private即可。
只要方法是私有的,类的设计者就可以确信它不会在别处使用,所以可以将其删去。如果一个方法是公共的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法。
4.3.11 final实例字段
可以将实例字段定义为final,这样的字段必须在构造对象时初始化。也就是说,必须确保在买一个构造器执行之后,这个字段的值已经设置,并且以后不能再修改这个字段。
final修饰符对于类型为基本类型或者不可变类的字段尤其有用(例如,String类是不可变的)。
对于可变的类,使用final修饰符可能会造成混乱。例如:
private final StringBuilder evaluations;
它在Employee构造器中初始化为
evaluations = new StringBuilder();
final关键字只是表示存储在evaluations变量中的对象引用不会再指示另一个不同的StringBuilder对象。不过这个对象可以更改:
public void giveGoldStar(){
evaluations.append(LocalDate.now()+":Gold star!\n");
}
4.4 静态字段与静态方法
4.4.1 静态字段
如果将一个字段定义为static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。静态字段属于类,不属于任何单个对象。
4.4.2 静态常量
静态变量使用得比较少,但静态常量却很常用。
4.4.3 静态方法
静态方法是不在对象上执行的方法。例如Math类的pow方法就是一个静态方法,表达式Math.pow(x,a)
会计算幂,在完成运算时,它并不使用任何Math对象。换句话说,它没有隐式参数,可以认为静态方法是没有this参数的方法(在一个非静态的方法中,this参数指示这个方法的隐式参数)。
静态方法不能访问实例字段,因为它不能在对象上执行操作。但是,静态方法可以访问静态字段。建议使用类名调用静态方法。
public static int getNextId(){
return nextId; //returns static field
}
//可以提供类名来调用这个方法:
int n = Employee.getNextId();
在下面两种情况下可以使用静态方法:
- 方法不需要访问对象状态,因为它需要的所有参数都通过显示参数提供(例如:Math.pow)。
- 方法只需要访问类的静态字段(例如:Employee.getNextId)。
C++注释:Java中的静态字段与静态方法在功能上与C++相同。但是,语法上稍有所不同。在C++中,要使用::操作符访问作用域之外的静态字段和静态方法,如Math::PI。
C++第三次重用了static关键字,指示属于类而不属于任何类对象的变量和函数。这个含义与Java中这个关键字的含义相同。
4.4.4 工厂方法
4.4.5 main方法
可以调用静态方法而不需要任何对象。例如,不需要构造Math类的任何对象就可以调用Math.pow。
main方法也是一个静态方法。
public class Application{
public static void main(String[] args){
//construct objects here
...
}
}
main方法不对任何对象进行操作。事实上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。
Java.util.Objects
- static < T > void requireNonNull(T obj)
- static < T > void requireNonNull(T obj,String message)
- static < T > void requireNonNull(T obj,Supplier < String > meaageSupplier)
如果obj为null,这些方法会抛出一个NullPointerException异常而没有消息或者给定的消息。- static < T > T requireNonNullElse(T obj,T defaultObj)
- static < T > T requireNonNullElseGet(T obj,Supplier < T > defaultSupplier)
如果obj不为null则返回obj,或者如果obj为null则返回默认对象。
4.5 方法参数
- 按值调用表示方法接收的是调用者提供的值。
- 引用调用表示方法接收的是调用者提供的变量地址。
方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。
Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个副本。具体来讲,方法不能修改传递给它的任何参数变量的内容。
Java程序设计语言对对象采用的不是按引用调用,实际上,对象引用是按值传递的。
Java中的方法参数:
- 方法不能修改基本数据类型的参数(即数值型或布尔型)。
- 方法可以改变对象参数的状态。
- 方法不能让一个对象参数引用一个新的对象。
C++注释:C++中有按值调用和按引用调用。引用参数标有&符号。例如,可以轻松地实现void tripleValue(double& x)方法或void swap(Employee& x,Employee& y)方法来修改它们的引用参数。
4.6 对象构造
由于对象构造非常重要,所以Java提供了多种编写构造器的机制。
4.6.1 重载
如果多个方法有相同的名字、不同的参数,便出现了重载。
方法的签名(signature):方法名和参数类型。例如,String类有4个名为indexOf的公共方法。它们的签名是
indexOf(int)
indexOf(int,int)
indexOf(String)
indexOf(String,int)
返回类型不是方法签名的一部分。也就是说,不能有两个名字相同、参数类型也相同却有不同返回类型的方法。
4.6.2 默认字段初始化
如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。
注释:这是字段与局部变量的一个重要区别。方法中的局部变量必须明确地初始化。但是在类中,如果没有初始化类中的字段,将会自动初始化为默认值(0、false或null)。
4.6.3 无参数的构造器
无参构造器创建对象时,对象的状态会设置为适当的默认值。
如果写一个类时没有编写构造器,就会提供一个无参数构造器。这个构造器将所有的实例字段设置为默认值。
仅当类没有任何其他构造器的时候,才会得到一个默认的无参数构造器。
4.6.4 显示字段初始化
可以在类定义中直接为任何字段赋值。例如:
class Employee{
private String name = "";
...
}
4.6.5 参数名
参数变量会遮蔽同名的实例字段。可以用this.xxx访问实例字段。this指示隐式参数,也就是所构造的对象。
public Employee(String name,double salary){
this.name = name;
this.salary = salary;
}
4.6.6 调用另一个构造器
关键字this指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。例子:
public Employee(double s){
//calls Employee(String,double)
this("Employee #" + nextId, s);
nextId++;
}
当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String,double)构造器。
采用这种方式使用this关键字非常有用,这样对公共的构造器代码只需要编写一次即可。
C++注释:在Java中,this引用等价于C++中的this指针。但是,在C++中,一个构造器不能调用另一个构造器。在C++中,必须将抽取出的公共初始化代码编写成一个独立的方法。
4.6.7 初始化块
初始化数据字段的方法:
- 在构造器中设置值
- 在声明中赋值
- 初始化块(initialization block)
在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。
首先运行初始化块,然后才运行构造器的主体部分。
建议总是将初始化块放在字段定义之后。
如果类的静态字段需要很复杂的初始化代码,那么可以使用静态的初始化块。
将代码放在一个块中,并标记关键字static。如下示例,功能是将员工ID的起始值赋予一个小于10000的随机整数。
//static initialization block
static{
var generator = new Random();
nextId = generator.nextInt(10000);
}
在类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式地设置成其他值,否则默认的初始值是0、false或null。所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。
class Employee{
private static int nextId;
private int id;
private String name = ""; //instance field initialization
private double salary;
//static initialization block
static{
var generator = new Random();
//set nextId to a random number between 0 and 9999
nextId = generator.nextInt(10000);
}
//object initialization block
{
id = nextId;
nextId++;
}
Java.util.Random
- Random()
构造一个新的随机数生成器- int nextInt(int n)
返回一个0~n-1之间的随机数
4.6.8 对象析构与finalize方法
由于Java会完成自动的垃圾回收,不需要人工回收内存,所以Java不支持析构器。
如果一个资源一旦使用完就需要立即关闭,那么应当提供一个close方法来完成必要的清理工作。
警告:不要使用finalize方法来完成清理。这个方法原本要在垃圾回收器清理对象之前调用。不过,你并不能知道这个方法到底什么时候调用,而且该方法已经被废弃。
4.7 包
Java允许使用包(package)将类组织在一个集合中。
4.7.1 包名
使用包的主要原因是确保类名的唯一性。
为了保证包名的绝对唯一性,要用一个因特网域名(这显然是唯一的)以逆序的形式作为包名,然后对于不同的工程使用不同的子包。
注释:从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util包与java.util.jar包毫无关系。每一个包都是独立的类集合。
4.7.2 类的导入
一个类可以使用所属包中的所有类,以及其他包中的公共类(public class)。
访问另一个包中的公共类:
-
使用完全限定名,就是包名后面跟着类名。例如:
java.time.LocalDate today = java.time.LocalDate.now();
-
使用import语句
import java.time.*; //import java.time.LocalDate; LocalDate today = LocalDate.now();
如果两个Date类都需要使用,在每个类名的前面加上完整的包名。
var deadline = new java.util.Date();
var today = new java.sql.Date(...);
在包中定位类是编译器(compiler)的工作。类文件中的字节码总是使用完整的包名引用其他类。
4.7.3 静态导入
有一种import语句允许导入静态方法和静态字段,而不只是类。例如:
import static java.lang.System.*;
out.println("Goodbye, World!"); //i.e., System.out
exit(0); //i.e., System.exit
4.7.4 在包中增加类
如果没有在源文件中放置package语句,这个源文件中的类就属于无名包(unnamed package)。无名包没有包名。
编译器处理文件(带有文件分隔符和扩展名.java的文件),而Java解释器加载类(带有.分隔符)。
编译器在编译源文件的时候不检查目录结构。
4.7.5 包访问
public:可以由任意类使用。
private:只能由定义它们的类使用。
没有指定public和private,这个部分(类、方法、变量)可以被同一个包中的所有方法访问。
4.7.6 类路径
类存储在文件系统的子目录中。类的路径必须与包名匹配。
类路径是所有包含类文件的路径的集合。
类文件也可以存储在JAR(Java归档)文件中。
JAR文件使用ZIP格式组织文件和子目录。可以使用任何ZIP工具查看JAR文件。
4.8 JAR文件
4.9 文档注释
4.10 类设计技巧
-
一定要保证数据私有
绝对不要破坏封装性。最好保持实例字段的私有性。 -
一定要对数据进行初始化
Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据。 -
不要在类中使用过多的基本类型
用其他的类替换使用多个相关的基本类型。例如,用一个名为Address的新类替换一个Customer类中以下的实例字段:private String street; private String city; private String state; private int zip;
-
不是所有的字段都需要单独的字段访问器和字段更改器
-
分解有过多职责的类
-
类名和方法名要能够体现它们的职责
对于方法,访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。 -
优先使用不可变的类