本文章是在学习《java核心技术》时记录的笔记
一、对象与类
面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,它取代了20世纪70年代的“结构化”或过程式编程技术。由于java是面向对象的,所以必须熟悉OOP才能够很好地使用java。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。程序中的很多对象来自标准库,还有一些是自定义的。究竟是自己构造对象,还是从外界购买对象完全取决于开发项目的预算和时间。从根本上讲,只要对象能够满足要求,就不必关心其功能到底是如何实现的。
1、类
类(class)是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的模具,将对象想象为小甜饼。由类构造对象的过程称为创建类的实例。
正如前面所看到的,用java编写的所有代码都位于某个类里面。标准java库提供了几千个类,可用于各种目的,如用户界面设计、日期等。尽管如此,在java中还是需要创建一些自己的类,以便描述你的应用程序所对应的问题领域中的对象。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特定的实力字段值。这些值的集合就是这个对象的当前状态(state)。如论何时,只要在对象上调用一个方法,它的转态就有可能发生改变。
实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实力字段。程序只能通过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。这意味着一个类可以完全改变存储数据的方式,只要仍旧使用同样的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化。
2、对象
对象的三个特性:行为、状态、标识。
同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法来定义的。此外,每个对象都保存着描述当前状况的信息,这就是对象的状态,对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。对象转态的改变必须通过方法实现(如果不经过方法调用就可以改变对象转态,只能说明破坏了封装性)。
3、识别类
传统的过程式程序中,必须从顶部的main函数开始编写程序。在面向对象程序设计时没有所谓的“顶部”。学习OOP的初学者因此会感到无从下手。答案是:首先识别类开始,然后再为各个类添加方法。
识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词。
4、类之间的关系
在类之间,最常见的关系有:依赖、聚合、继承。
依赖:如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该经可能地将互相依赖的类减至最少。
聚合:意味着类A的对象包含类B的对象。
继承:一般而言,如果类A扩展类B,类A不但包含从类B继承的方法,还会有一些额外的功能。
二、使用预定义类
在java中,没有类就无法做任何事情,我们前面曾经接触过几个类。然而,并不是所有的类都表现出面向对象的典型特征。例如,Math类。你已经看到,可以直接使用Math类的方法,如Math.random,而不必了解它具体是如何实现的,你只需要知道方法名和参数。这正式封装的关键所在,当然所有类都是这样的,但是Math类只封装了功能,它不需要也不必隐藏数据,由于没有数据,因此也不必考虑创建对象和初始化他们的实力字段,因为根本没有实例字段!
1、对象与对象变量
要想使用对象,首先必须构造对象,并指定其初始状态。然后对对象应用方法。
在java程序设计语言中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
接下来用java中的Date类举例。
构造器的名字应该与类名相同,因此Date类的构造器名为Date。要想构造一个Date对象,需要在构造器前面加上new操作符。
具体实例:
import java.lang.reflect.Array;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Date;
public class Main {
public static void main(String[] args) {
new Date(); //构建了一个新对象,这个对象被初始化为当前的日期和时间
System.out.println(new Date()); //Fri Oct 04 16:39:02 CST 2024
}
}
或者也可以对刚刚创建的对象应用一个方法。Date类中有一个toString方法,这个方法将返回日期的字符串描述。接下来将展示,如果对新构造的Date对象应用toString方法。
实例如下:
import java.util.Date;
public class Main {
public static void main(String[] args) {
String date=new Date().toString();
System.out.println(date);
}
}
刚刚展示例子中,构造的对象仅使用了一次,通常,你会希望构造的对象可以多次使用,因此,需要将对象存放在一个变量中。
实例如下:
Date date=new Date();
注意事项: 用变量保存对象的时候,必须首先初始化变量deadline,这里有两个选择,当然,可以初始化这个变量,让它引用一个新构造的对象:
Date date=new Date();
也可以设置这个变量,让它引用一个已有的对象:
Date date=new Date();
Date birthday=date;
解释:
这是date、birthday两个变量都引用同一个对象。要认识到重要的一点:对象变量并没有实际包含一个对象,它只是引用一个对象。
在java中,任何对象变量的值都是对存储在另外一个地方的某个对象的引用,new操作符的返回值也是一个引用。
例如:
Date deadline=new Date();
解释:
有两个部分,表达式new Date()构造了一个Date类型的对象,它的值是对新创建对象的一个引用。这个引用存储在变量deadline中。
可以显示地将对象变量设置为null,指示这个对象变量目前没有引用任何对象。
``
三、用户自定义类
现在来学习如何编写复杂应用程序所需要的那种主力类(workhorse class)。通常,这些类没有main方法,却有自己的实例字段和实例方法。要想构建一个完整的程序,会结合使用多个类,其中只有一个类有main方法。
1、Empolyee类
具体实例:
import java.time.LocalDate;
import java.util.Date;
public class Main {
public static void EmployeeTest(String[] args) {
//用单个雇员对象填满staff数组
Employee[] staff=new Employee[3];
staff[0]=new Employee("xiaohu",8000,2002,12,16);
staff[1]=new Employee("xiaoli",8000,2006,12,16);
staff[2]=new Employee("xiaozhang",8000,2003,12,16);
``
//将每个员工的工资上涨5%
for (Employee e:staff)
e.raiseSalary(5);
``
//输出每个雇员对应的所有信息
for(Employee e:staff)
System.out.println("name="+e.getName()+",salary="+e.getSalary()+",hireDay="+e.getHireDay());
}
}
/**
\* Employee
*/
class Employee {
//定义实例属性
private String name;
private double salary;
private LocalDate hireDay;
``
public Employee(String n,double s,int year,int month,int day){
name=n;
salary=s;
hireDay=LocalDate.of(year, month, day);
}
public String getName(){
return name;
}
public double getSalary(){
return salary;
}
public LocalDate getHireDay(){
return hireDay;
}
public void raiseSalary(double byPercent){
double raise=salary * byPercent/100;
salary+=raise;
}
}
解释:
在这个程序中,我们构造了一个Employee数组,并填入了3个Employee对象。接下来使用Employee类的raiseSalary方法将每个员工的薪水提高5%。最后,使用getName方法、getSalary方法和getHireDay方法打印各个员工的信息。接下来,当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class和Employee.class。将程序中包含main方法的类名提供给字节码解释器,以启动这个程序。字节码解释器开始运行EmployeeTest类的main方法中的代码,在这段代码中,先后构造了3个新Employee对象,并显示他们的转态。
public意味着任何类的任何方法都可以调用这些方法。然后定义了三个实例字段,关键字private确保只有Employee类自身的方法能够访问这些实例字段,而其他类的方法不能够读写这些字段。
注意事项:
源文件文件名是EmployeeTest.java。这是因为文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公共类,但可以有任何数目的非公共类。
构造器的解释
public Employee(String n,double s,int year,int month,int day){
name=n;
salary=s;
hireDay=LocalDate.of(year, month, day);
}
解释:
可以看到,构造器与类同名,在构造Employee类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始转态。
构造器与其他方法有一个重要的不同。构造器总是结合new运算符来调用。不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的。构造器没有返回值
2、多个源文件的使用
一个源文件包含了两个类,许多程序员习惯将每一个类存放在一个单独的源文件中。例如将Employee类存放在文件Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。
如果这样组织文件,可以有两种编译源程序的方法。一种是使用通配符调用java编译器:
javac Employee*.java
这样一来,所有与通配符匹配的源文件都被编译成类文件。
或者输入以下命令:
javac EmployeeTest.java,
使用这种方式并没有显式地编译Employee.java。不过,当java编译器发现EmployeeTest.java使用Employee类时,它会查找名为Employee.class的文件。如果没有找到这个文件,就会自动搜索Employee.java,然后,对它进行编译。
3、使用var声明局部变量
在java10中,如果可以从变量的初始值推导出他们的类型,那么可以用var关键字声明局部变量,而无须指定类型。
注意:
var关键字只能用于方法中的局部变量。参数和字段的类型必须声明。
4、基于类的访问权限
方法可以访问调用这个方法的对象的私有数据。一个方法可以访问所属类的所有对象的私有数据。
5、私有方法
在实现一个类的时候,由于公共数据非常危险,所以应该将所有的数据字段都设置为私有的。然而,方法又应该如何设计呢?尽管绝大多数方法都被设计为公共的,但在某些特殊情况下,将方法设计为私有可能很有用。有时,你可能希望将一个计算代码分解成若干个独立的辅助方法。通常,这些辅助方法不应该成为公共接口的一部分,这是由于它们往往与当前实现关系非常紧密。或者需要一个特殊协议或者调用次序。
在java中,要实现私有方法,只需要将关键字public改为private即可。
通过将方法设计为私有,如果你改变了方法的实现方式,将没有义务保证这个方法依然可用。如果数据的表示发生了变化,这个方法可能会变得难以实现,或者不再需要;这并不重要,重点在于,只要方法是私有的,类的设计者就可以确信它不会在别处使用,所以可以将其删去。如果一个方法是公共的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法。
6、final实例字段
可以将实例字段定义为final。这样的字段必须在构造对象时初始化,也就是说,必须确保在每一个构造器执行之后,这个字段的值已经设置,并且以后不再修改这个字段。
实例如下:
结合上文举出的实例,在定义字段的可以将name实例字段用final修饰,表明name字段必须进行初始。
class Employee
{
private final String name;
......
}
final修饰符对于类型为基本类型或者不可变类的字段尤其有用。(如果类中的所有方法都不会改变其对象,这样的类就是不可变的类。例如,String类就是不可变的)
四、静态字段与静态方法
1、静态字段
如果将一个字段定义为static,每个类只有一个这样的字段。而对非静态的实例字段,每个对象都有自己的一个副本。静态实例字段属于类,而不属于任何单个的对象。
实例:
private static int number=1;
2、静态常量
静态变量使用得比较少,但静态常量却很常用。
实例:
public static final double pi=3.122323;
补充:
在上文中曾经多次提到,由于每个类对象都可以修改公共字段,所以,最好不要有公共字段,然而,公共常量(即final字段)却没有问题。因为out被声明为final,所以,不允许再讲他重新赋值为另一个打印流。
3、静态方法
静态方法是不在对象上执行的方法,但是静态方法可以访问静态字段。静态方法是没有this参数的方法(在一个非静态的方法中,this参数指示这个方法的隐式参数)。
实例如下:
public static int getNextId()
{
return nextId; //return static filed
}
可以提供类名来调用这个方法:
int n=Employee.getNexted();
使用静态方法的场景:
1、方法不需要访问对象状态,因为它需要的所有参数都通过显式参数提供。
2、方法只需要访问类的静态字段;
4、工厂方法
静态方法还有另外一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。
5、main方法
需要注意,可以调用静态方法而不需要任何对象。main方法不对任何对象进行操作,实际上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。
五、方法参数
首先回顾一下再程序设计语言中关于如何将参数传递给方法(或函数)的一些专业术语。
1、按值调用(call by value):表示方法接收的是调用者提供的值。
2、引用调用(call by reference):表示方法接收的是调用者提供的变量地址。
方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。
java程序设计语言总是采用按值调用,也就是说,方法得到的是所有参数值的一个副本。具体来说,方法不能修改传递给它的任何参数变量的内容。
六、对象构造
文章前半部分已经介绍了编写简单的构造器,可以定义对象的初始状态。但是,由于对象构造非常重要,所以java提供了多种编写构造器的机制。
1、重载
有些类有多个构造器。例如,可以如下构造一个空的StringBuilder对象:
var messages=new StringBuilder();
或者,可以指定一个初始字符串:
var todoList =new StringBuilder("To do:\n");
这种功能叫做重载。如果多个方法有相同的名字、不同的参数,便出现重载。编译器必须挑选出具体调用哪个方法。它用各个方法首部中的参数类型与特定方法调用中所使用的值类型进行匹配,来选出正确的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他更好的。(这个查找匹配的过程称为重载解析)
2、默认字段初始化
如果在构造器中没有显式地为字段设置初值,那么就会被自动地赋为默认值:数值为0,布尔值为false,对象引用为null。有些人认为依赖默认值的做法是一种不好的编程实践。如果不明确地对字段进行初始化,就会影响程序代码的可读性。
补充:
这是字段与局部变量的一个重要区别。方法中的局部变量必须明确的初始化。但是在类中,如果没有初始化类中的字段,将会自动初始化为默认值(0,null或false)。
3、无参数的构造器
很多类中都包含一个无参数的构造器,由无参数构造器创建对象时,对象的状态会设置为适当的默认值。
如果写一个类时没有编写构造器,就会为你提供一个无参数构造器,这个构造器将所有的实力字段设置为默认值。于是,实例字段中的数值型数据设置为0,布尔型数据设置为false,所有对象变量将设置为null。
实例:
public Employee()
{
name="";
salary=0;
hireDay=LocalDate.new();
}
解释:
这个构造器是无参构造器,将对象的状态设置为默认值。
注意:
如果类中提供了至少一个构造器,但是没有提供无参数的构造器,那么构造对象时,如果不提供参数就是不合法的,
4、显式字段初始化
通过重载类的构造器方法,可以采用多种形式设置类的实力字段的初始状态。不管怎样调用构造器,每个实例字段都要设置为一个有意义的初值,确保这一点总是一个好主意。
可以在类定义中直接为任何字段赋值。在执行构造器之前先完成这个赋值操作,如果一个类的所有构造器都希望把某个特定实例字段设置为同一个值,这个语法就特别有用。
初始值不一定是常量值,可以利用方法调用初始化一个字段。
实例如下:
class Employee
{
private static int nextId;
private int id=assignId();
private static int assignId()
{
int r=nextId;
nextId++;
return r;
}
}
5、参数名
在编写很小的构造器时,常常在参数命名时感到困惑。我们通常用单个字母作为参数名,但是这样的缺点是:只有阅读代码才能了解参数n和参数s的含义。
这里有一种常用的技巧:参数变量会遮蔽同名的实例字段。例如,如果将参数命名为salary,salary将指示这个参数,而不是实例字段。但是,还是可以用this.salary访问实例字段。this指示隐式参数,也就是构造的对象。
实例:
public Employee(String name,double salary)
{
this,name=name;
this.salary=salary;
}
6、调用另一个构造器
关键字this指示一个方法的隐式参数。不过,这个关键字还有另外一个含义。
如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。
实例:
public Employee(double s)
{
//calls Employee(String,double)
this ("Employee#"+nexeId,s);
nextId++;
}
当调用new Employee(600)时,Employee(double)构造器将调用Employee(String,double)构造器。采用这种方式使用this关键字非常有用,这样对公共的构造器代码只需要编写一次即可。
7、初始化块
前面已经讲过两种初始化数据字段的方法:
在构造器中设置值;
在声明中赋值。
实际上,java还有第三种机制,称为初始化块。在一个类的声明中,可以包含任何多个代码块。只要构造这个类的对象,这些块就会被执行。
七、包
java允许使用包将类组织在一个集合中。借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
1、包名
使用包的主要原因是确保类名的唯一性,如当个程序员不约而同地建立了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,要用一个因特网域名以逆序的形式为包名,然后对于不同的工程使用不同子包。如:hostname.com,应该逆序写成:com.hostname。然后可以追加一个工程名,如:com.hostname.corejava。如果在把Employee类放在这个包里,那么这个类的“完全限定”名就是com.hostname.corejava.Employee。
2、类的导入
一个类可以使用所属包中的所有类,以及其他包中的公共类。
我们可以采用两种方式访问另一个包中的公共类。第一种方式就是使用完全限定名,即包名后面跟着类名。
实例如下:
java.time.LocalDate today=java.time.LocalDate.now();
另一种方法是使用import语句,import语句是一种引用包中各个类的简捷方式。一旦使用了import语句,在使用类时,就不必写出类的全名了。可以使用import语句导入一个特定的类或者整个包。import语句应该位于源文件的顶部(但位于package语句的后面)。
实例:
import java.time.* ; //导入了java.util包中的所有类
注意事项:
只能使用星号(*)导入一个包。
3、静态导入
有一种import语句允许导入静态方法和静态字段,而不只是类。
实例:
import static java.lang.System.*;
out.println("xiaohu");
4、在包中增加类
要想将类放入包中,就必须将包的名字放在源文件的开头,即放在定义这个包中各个类的代码之前。
实例:
package com.hostname.corejava; //放包的名称,这样就可以使用这个包中的类
如果没有在源文件中放置package语句,这个源文件中的类就属于无名包。无名包没有包名。到目前为止,我们定义的所有类都在这个无名包中。将源文件放到与完整包名匹配的子目录中。
5、包访问
前面已经接触过访问修饰符public和private。标记为public的部分可以由任意类使用;标记为private的部分只能由定义它们的类使用,如果没有指定public或private,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
6、类路径
在前面已经看到,类存储在文件系统的子目录中,类的路径必须与包名匹配。另外,类文件也可以存储在JAR文件中。在一个JAR文件中,可以包含多个压缩形式的类文件和子目录,这样既可以节省空间又可以改善性能。在程序中用到第三方的库文件时,通常要得到一个或多个需要包含的JAR文件。
八、JAR文件
在将应用程序打包时,你一定希望只向用户提供一个单独的文件,而不是一个包含大量类文件的目录结构,java归档(JAR)文件就是为此目的而设计的。一个JAR文件既可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的zip压缩格式。