《Java核心技术 卷Ⅰ》 第4章 对象与类html
传统的结构化程序设计:首先肯定如何操做数据,再决定如何组织数据。前端
面向对象程序设计:将数据放在第一位,再考虑操做数据的算法。java
类(class)是构造对象的模板或蓝图,
由类构造(construct)对象的过程称为建立类的实例(instance)。
封装(encapsulation),也称数据隐藏,封装将数据和行为组合在一个包中,并对对象使用者隐藏数据实现方式,对象中的数据域称为实例域(instance field),操做数据的过程称为方法(method)。git
对于每一个特定的类实例(对象)都有一组特定的实例域值,这些值的集合就是这个对象的当前状态(state),只要向对象发送一个消息,它的状态就有可能发生改变。程序员
实现封装的关键:绝对不能让类中的方法直接地访问其余类的实例域。github
OOP的另外一个原则:能够经过扩展一个类来创建另一个新的类。在Java中,全部类都源于一个超类——Object
。算法
在扩展一个已有类时,新类具备这个类的所有属性和方法,在新类中,只须要提供适用于这个新类的新方法和数据域就能够了,这个过程称为继承(inheritance)。sql
对象的三个主要特性:安全
识别类的简单规则:并发
常见的关系有:
Customer "use-a" MobilePhone
。Programmer "has-a" Coffee
。Student "is-a" Person.
。想要使用对象,就必须首先构造对象,并指定其初始状态,
而后对对象应用方法。
在Java中,使用构造器(constructor)构造新实例,它是一种特殊的方法,用于构造并初始化对象。
Date birthday = new Date(); String s = birthday.toString();
对象变量并无实际包含一个对象,而仅仅是一种引用,在Java中,任何对象变量的值都是对存储在另一个地方的一个对象的引用,new操做符的返回值也是一个引用。
当一个对象变量只是声明可是没有具体的引用对象时,调用其方法会在编译时产生变量未初始化错误。
// Error test P1 Date deadline; deadline.toString();
当一个对象变量只是声明可是没有具体的引用对象时,调用其方法会产生运行时错误(一般为java.lang.NullPointerException
)。
// Error test P2 Date deadline = null; deadline.toString();
上面两个例子说明,Java中的局部变量并不会自动地初始化为null
,而必须经过调用new
或将他们设置为null
进行初始化。
Date类的实例有一个状态,即特定的时间点。
时间是距离纪元(epoch)的毫秒数(可正可负),纪元是UTC(Coordinated Universal Time)时间1970年1月1日 00:00:00。
类库设计者把保存时间与给时间点命名分开,因此标准Java类库分别包含了两个类:
Date
类LocalDate
类不要使用构造器来构造LocalDate类的对象,应用静态工厂方法(factory method)表明调用构造器。
// 当前时间的对象 LocalDate.now(); // 指定时间的对象 LocalDate.of(1996, 6, 30); // 保存对象 LocalDate birthday = LocalDate.of(1996, 6, 30);
有了对象就可使用方法得到年、月、日。
int year = birthday.getYear(); // 1996 int month = birthday.getMonthValue(); // 6 int day = birthday.getDayOfMonth(); // 30 int dayOfWeek = birthday.getDayOfWeek().getValue(); // 7
须要计算某个日期时:
LocalDate someday = birthday.plusDays(708); int year = someday.getYear(); // 1998 int month = someday.getMonthValue(); // 6 int day = someday.getDayOfMonth(); // 8 // 固然还有minusDays方法
Java简单类的形式:
class ClassName { filed1 field2 ... constructor1 constructor2 ... method1 method2 ... }
一个使用简单类的程序例子:
// File EmployeeTest.java public class EmployeeTest { public static void main(String[] args) { Employee[] staff = new Employee[3]; staff[0] = new Employee("Bob Hacker", 75000, 1996, 6, 30); ... } } class Employee { // instance fields private String name; private double salary; private LocalDate hireDay; // constructor public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } // methods public String getName() { return name; } ... }
注意,这个程序中包含两个类:
Employee
类public
访问修饰符的EmployeeTest
类源文件名是EmployeeTest.java
,这是由于文件名必须与public类的名字相匹配,在一个源文件中,只能有一个公有类,但能够有任意个非公有类。
当编译这段源码时,编译器会在目录下生成两个类文件:EmployeeTest.class
和 Employee.class
。
将程序中包含main方法的类名提供给字节码解释器,启动程序:
java EmployeeTest
字节码解释器开始运行其中的main方法的代码。
<!-- ### 多个源文件的使用
当习惯把各种单独放在一个文件中时,好比上面的程序中,建立一个文件Employee.java
单独存放这个类,可是在编译时有两种方法。
一是使用通配符将全部文件编译:
javac Employee*.java
一种是编译包含main方法的类:
javac EmployeeTest.java
后一种方法并无显式地编译Employee.java
,当Java编译器发现EmployeeTest.java
使用了Employee
类时,会查找名为Employee.class
的文件,若是没有找到,就会自动搜索Employee.java
,而后自动的进行编译。
而且当Employee.java
文件更新后,Java编译器在编译时会自动地从新编译该文件。 -->
刚才所使用类中的构造器:
class Employee { ... public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } ... }
构造器与类同名,在构造Employee
类对象时,对应的构造器会运行,执行代码将实例域初始化所指定的状态。
构造器与方法的其中一个不一样是,构造器老是伴随new
操做符的执行被调用,而且不能对已经存在的对象调用构造器来从新设置实例域。
Employee bob = new Employee("Bob", 47000, ...); bob.Employee("Bob", 47500, ...); // Compiler Error: Can't find symbol of method Person(String, int, ...)
构造器基础的简单总结:
方法用于操做对象以及存取他们的示例域。
public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
当对象调用方法时
bob.raiseSalary(5);
raiseSalary
方法有两个参数。
Employee
类的对象。在每个方法中,关键字this
表示隐式参数,上面的方法也能够写为:
public void raiseSalary(double byPercent) { // double raise = salary * byPercent / 100; double raise = this.salary * byPercent / 100; // salary += raise; this.salary += raise; }
有些人偏心这样写(包括我),虽然费事点,可是能够将实例域与局部变量明显的区分开来。
封装对于直接简单的公有数据而言,提供了更多对公有数据保护的途径。
对于访问器来讲,它们只返回实例域的值,而且在处理可引用的返回对象时,要经过clone
来建立新的对象来做为返回值的载体,若是将可引用对象直接返回,而且该对象恰有一个可修改值的方法时,任何外部对这个返回值的处理都将会直接影响到这个对象内部的对象(Java引用在这部分的状况相似与C中的指针)。
对于更改器来讲,它们在被调用时能够主动的执行数据合法性的检查,从而避免破坏数据的合法性。
方法能够访问所调用对象的私有数据。
可是Java其实还要更进一步:一个方法能够访问所属类的全部对象的私有数据。
// class class Employee { public boolean equals(Employee other) { return name.equals(other.name); } } ... // main if(harry.equals(boss)) ...
这个方法访问harry
的私有域,同时它还访问了boss
的私有域,这是合法的,boss
也是Employee
类对象,Employee
类的方法能够访问Employee
类的任何一个对象的私有域。
有时候为了完成任务须要写一些辅助方法,这些辅助方法不该该称为公有接口的一部分,这是因为它们与当前的实现机制很是紧密,最好将这样的方法设计为private
。
简单来讲,为了更好地封装性,不在公有接口范围内的方法都应该设计为private
。
类中能够定义实例域为final
,可是必须确保在每个构造器执行以后,这个域的值会被设置,并在后面的操做中不能再对其进行修改。
可是这里的不能修改大都应用于基本(primitive)类型和不可变(immutable)类型的域(若是类中每一个方法都不会改变对象状态,则类就是不可变的类,例如String
类)。
对于可变的类(好比以前的StringBuilder
类),使用final
修饰符只是表示该变量的对象引用不会再指示其余的对象,但其对象自己是能够更改的(好比StringBuilder
类的对象执行append
方法)。
若是将一个域定义为
static
,每一个类中只有这样的一个域。
通俗来说,若是一个域被定义为static
,那么这个域属于这个类,而不属于任何这个类的对象,这些对象同时共享这个域(有点像类的一个全局变量域)。
一个简单的静态域用法:
// class Employee ... // 能够在类定义中直接对静态域赋予一个初值。 private static int nextId = 1; private int id; ... public void setId() { id = nextId; nextId++; }
静态常量相比于静态变量使用的要多一些。
例如Math
类中的PI
:
public class Math { ... publuc static final double PI = 3.14159265358979323846; ... }
程序经过Math.PI
的形式得到这个常量。
静态方法是一种不能向对象实施操做的方法。
静态方法在调用时,不使用任何实例对象,换句话说就是没有隐式参数。
须要使用静态方法的状况:
好比以前LocalDate
类使用的静态工厂方法(factory method)来构造对象。
不利用构造器完成这个操做的两个缘由:
main
方法不对任何对象进行操做,由于事实上在启动程序时尚未任何一个对象,静态的main
方法将随着执行建立程序所须要的对象。
同时,每个类均可以有一个main
方法,经常使用于进行类的单元测试。
在程序设计语言中有关参数传递给方法(函数)的一些专业术语:
Java程序设计语言老是采用按值调用,即方法获得的只是参数值的一个拷贝,不能修改传递给它的任何参数变量的内容。
可是当对象引用做为参数时,状况就不一样了,方法得到的是对象引用的拷贝,对象引用和其余拷贝同时引用同一个对象。
可是这并非引用调用。
public static void swap(Obejct a, Obejct b) { Object tmp = a; a = b; b = tmp; }
若是Java在对象参数时采用的是按引用调用,上述方法就能实现交换数据的效果。
可是这里的swap
方法并无改变存储在调用参数中的对象引用,swap
方法的参数a
和b
被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝的引用。
Java中方法参数总结:
有些类有多个多个构造器。
这种特征叫作重载(overloading),若是多个方法有相同的名字、不一样的参数,便产生了重载。
编译器经过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法,若是编译器找不到匹配的参数,就会产生编译时错误。
Java容许重载任何方法,并不仅是构造器,所以要完整地描述一个方法,须要指出方法名以及参数类型,这叫方法的签名(signature)。
// 方法重载的签名举例 indexOf(int) indexOf(int int) indexOf(String)
能够看出,返回类型并非方法签名的一部分,即不能有两个名字相同、参数类型相同可是却返回不一样类型值的方法。
若是域没有在构造器中被赋予初值,则会被自动地赋予默认值:
这与局部变量的声明不一样,局部变量必须明确的进行初始化。
构造器中若是不明确地进行初始化,会影响代码的可读性。
若是在编写一个类时没有编写构造器,那么系统会提供一个无参数构造器,这个构造器将全部的实例域设置为默认值。
若是类中提供了至少一个构造器,可是没有提供无参数构造器,则构造对象时若是没有提供参数就会被视为不合法。
经过重载类的构造器方法,能够采用多种形式设置类的实例域的初始状态。
能够在类定义中,直接讲一个值赋予给任何域。
class Employee { private String name = ""; ... }
在构造器执行以前,先执行赋值操做。
初始值也能够不用是常量。
class Employee { private static int nextId; private int id = assignId(); ... private static int assignId() { int r = nextId; nextId++; return r; } ... }
上面的例子中,能够调用方法对域进行初始化。
在编写很小的构造器时,一般用单个字符命名:
public Employee(String n, double s) { name = n; salary = s; }
这样的缺陷是失去了代码可读性,也能够采用加前缀的方法:
public Employee(String aName, double aSalary) { name = aName; salary = aSalary; }
固然还有一种技巧,参数变量用一样的名字将实例域屏蔽起来:
public Employee(String name, double salary) { this.name = name; this.salary = salary; }
若是构造器的第一个语句形如this(...)
,这个构造器将调用同一个类的另外一个构造器。
public Employee(double s) { // calls Employee(String, double) this("Employee #" + nextId, s); nextId++; }
除了前面提到的两种初始化数据域的方法:
还有第三种,称为初始化块(initialization block),在类定义中能够包含多个代码块,只要构造类的对象,这些块就会被执行。
class Employee { private static int nextId = 0; private int id; { id = nextId; nextId++; } ... }
不管哪一个构造器构造对象,初始化块都会执行,首先运行初始化块,而后才运行构造器的主体部分。
调用构造器的具体处理步骤:
初始化块比较经常使用于代码比较复杂的静态域初始化:
static { Random generator = new Random(); nextId = generator.nextInt(10000); }
Java容许使用包(package)将类组织起来。
借助于包能够方便地组织本身的代码,并将本身的代码与别人提供的代码库分开管理。
标准的Java类库分布在多个包中,包括java.lang
、java.util
和java.net
等。
使用包的主要缘由是确保类名的惟一性。
假如两个程序员都创建了Employee
类,只要将类放置在不一样的包中,就不会产生冲突。
从编译器角度来讲,嵌套的包之间没有任何关系。例如java.util
包与java.util.jar
包毫无关系。
一个类可使用所属包的全部类,以及其余包中的公有类(public class)。
可使用两种方式访问另外一个包中的公有类。
import
语句,能够导入一个特定的类或者整个包。// 添加包名 java.time.LocalDate today = java.time.LocalDate.now(); // import import java.util.*; // or import java.time.LocalDate 引入特定类 LocalDate today = LocalDate.now();
大多数状况下导入包便可,可是在发生命名冲突的时候,就要注意了,
import java.util.*; import java.sql.*; Date today; // Error
由于这两个包都有Date
类,编译器没法肯定是哪个包的Date
类,因此这个时候能够增长一个指定特定类的import
语句。
若是两个类都要使用时,就在每一个类名前加上完整地包名。
import
语句还增长了导入静态方法和静态域的功能。
import static java.lang.System.*; // 而后可使用System类的静态方法和静态域而没必要加前缀 out.println("Hohoho!"); // System.out.println()
另外,还能够导入特定的方法或域:
import stattic java.lang.System.out; out.println("Hohoho!");
想将一个类放入包中,就必须将包的名字放在源文件的开头。
package com.horstmann.corejava; public clas Employee { ... }
若是没有在源文件中放置package
语句,源文件中的类被放置在默认包(default package)中,默认包是一个没有名字的包。
通常须要把包中的文件放到与完整的包名匹配的子目录中。
例如package com.horstmann.corejava
包中的全部源文件,应该被放置在子目录com/horstmann/corejava
中。
public
的类、方法、变量能够被任意的类使用private
的类、方法、变量只能被定义他们的类使用JDK包含一个很用有的工具——javadoc
,它能够由源文件生成一个HTML文档。
在源代码中添加以专用的定界符/**
开始的注释,则能够容易地生成形式上专业的文档,相比于把文档和代码单独存放,修改代码的同时修改文档注释再从新运行javadoc
,就不会出现不一致的问题。
javadoc
从下面几个特性中抽取信息:
应该为这几部分编写注释,注释应该放在所描述特性的前面。
注释以/**
开始,以*/
结束。
每一个/**...*/
文档注释中使用自由格式文本(free-form text),标记由@
开始。
类注释必须放在import
语句以后,类定义以前。
/** * Just some comment words here * another comment line * what is this class for? */ public class Card { ... }
方法注释放在描述的方法前,除了通用标记,还可使用下面的标记:
@param
变量 描述:用于标记当前方法的参数部分的一个条目@return
描述:用于标记方法的返回部分@throws
类 描述:表示方法有可能抛出异常/** * Buy one coffee. * @param money the cost of coffee * @param coffeeTpye which coffee * @return coffee one hot coffee * @throws NoMoreCoffee */ public buyCoffee(double money, CoffeeType coffeeTpye) { ... }
只须要对公有域(一般是静态常量)建议文档。
/** * The ratio of a circle's circumference to its diameter */ public static final double PI = 3.1415926...;
可用在类文档的注释的标记:
@deprecated 文本:标记对类、方法或变量再也不使用,例如:
@deprecated Use <code> setVisible(true) </code> instead
@see 引用:增长一个超连接,能够用于类、方法中,引用有如下状况:
// 创建一个链接到com.horstmann.corejava.Employee类的raiseSalary(double)方法的超连接 @see com.horstmann.corejava.Employee#raiseSalary(double) // 能够省略包名,甚至把包名和类名省去 @see Employee#raiseSalary(double) // 此时连接定位于当前包 @see raiseSalary(double) // 此时链接定位于当前类
<a href="...">label</a>
@see <a href="m«w.horstmann.com/corejava.htinl">The Core ]ava home page</a> // 此处可使用label标签属性来添加用户看到的锚名称
@see "Core Java 2 volume 2n"
若是愿意的话,还能够在注释的任何位置放置指向其余类和方法的超连接:
{ @link package.class#feature label } // 这里的描述规则与@see标记规则同样
若是想要包的注释,就要在每个包的目录中添加一个单独的文件。
package.html
命名的文件,在<body>...</body>
之间的全部文本会被抽取。package-info.java
命名的文件,这个文件包含一个初始的以/**
和*/
界定的Javadoc
注释,跟随在一个包语句以后。还能够为全部的源文件提供一个概述性的注释,这个注释将被放置在一个名为overview.html
的文件中,这个文件位于包含全部源文件的父目录中,标记<body>...</body>
之间的全部文本会被抽取。当用户选择overview时,就会查看到这些注释内容。
应用这些技巧能够设计出更具备OOP专业水准的类。
绝对不要破坏封装性,这是最重要的。
数据的表示形式极可能会改变,可是它们的使用方式却不会常常发生变化,当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响,即便出现bug也易于检测。
Java不对局部变量进行初始化,可是会对对象的实例域进行初始化。
可是也最好不要依赖系统的默认值,应该用构造器或者是提供默认值的方式来显式地初始化全部的数据。
用其余的类代替多个 相关的基本类型的使用。
这样会使类更加易于理解和修改。
好比用一个Address
的类来代替下面的实例域:
private String street; private String city; private String state; private int zip;
这样更容易理解和处理表示地址的域,而使用这些域的类并不用去关心这些域是怎么具体变化的。
虽然这里的“过多”对于我的来讲是一个含糊的概念,可是若是明显地能够将一个复杂的类分解成两个更为简单的类,则应该进行分解。
命名类名的良好习惯是采用:
Order
RushOrder
BillingAddress
对于方法来讲:
get
开头set
开头更改对象的问题在于:若是多个线程视图同时更新一个对象,就会发生并发更改,其结果是不可预料的。若是类时不可变的,就能够安全地在多个线程间共享其对象。
我的静态博客: