对象导论
对象具有状态(对象的内部数据)、行为(方法)和标识(每一个对象在内存中都有唯一的地址)。
每个对象都有接口。
类描述了具有相同特性(数据元素)和行为(功能)的对象集合,所以一个类实际上就是一个数据类型。
必须有某种方式产生对对象的请求,使对象完成各种任务,如在屏幕上画图、打开开关等等。每个对象都只能满足某些请求,这些请求由对象的接口(interface)所定义,决定接口的便是类型。
每个对象都提供服务
当正在试图开发或理解一个程序设计时,最好的方法之一就是将对象想像为”服务提供者”。程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。
将对象看作是服务提供者还有一个附带的好处:它有助于提高对象的内聚性。高内聚是软件设计的基本质量要求之一:这意味着一个软件构件(例如一个对象,当然它也有可能是指一个方法或一个对象库)的各个方面”组合”得很好。在良好的面向对象设计中,每个对象都可以很好地完成一项任务,但是它并不试图做更多的事。
隐藏具体实现
将程序开发人员分为类创建者和客户端程序员。类创建者只向客户端程序员暴露必要的部分。隐藏其它部分,这样类创建者可以任意修改隐藏部分,不会对其他人造成影响。被隐藏的部分代表对象内部脆弱的部分,可以避免被客户端程序员随意使用,可以减少bug。
访问控制的第一个存在原因就是让客户端程序员无法触及他们不应该触及的部分-这些部分对数据类型的内部操作来说是必需的,但并不是用户解决特定问题所需的接口的一部分。这对客户端程序员来说其实是一项服务,因为他们可以很容易地看出哪些东西对他们来说很重要,而哪些东西可以忽略。
访问控制的第二个存在原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到客户端程序员。例如,你可能为了减轻开发任务而以某种简单的方式实现了某个特定类,但稍后发现你必须改写它才能使其运行得更快。如果接口和实现可以清晰地分离并得以保护,那么你就可以轻而易举地完成这项工作。
java中private关键字表示除类型创建者和类型的内部方法之外的任何人都不能访问的元素。protected关键字与private作用相当,差别仅在于继承的类可以访问protected成员,但是不能访问private成员。Java还有一种默认的访问权限,当没有使用前面提到的任何访问指定词时,它将发挥作用。这种权限通常被称为包访问权限,因为在这种权限下,类可以访问在同一个包(库构件)中的其他类的成员,但是在包之外,这些成员如同指定了private一样。protected成员也可以被同一个包的其它类访问。public表示紧随其后的元素任何人都可以访问。
复用具体实现
新的类可以由任意数量、任意类型的其他对象以任意可以实现新的类中想要的功能的方式所组成。因为是在使用现有的类合成新的类,所以这种概念被称为组合(composition),如果组合是动态发生的,那么它通常被称为聚合(aggregation)。组合经常被视为”has-a”(拥有)关系,就像我们常说的”汽车拥有引擎”一样。
在建立新类时,应该首先考虑组合,因为它更加简单灵活。
继承
有两种方法可以使基类与导出类产生差异。第一种方法非常直接:直接在导出类中添加新方法。应该仔细考虑是否存在基类也需要这些额外方法的可能性。
第二种也是更重要的一种使导出类和基类之间产生差异的方法是改变现有基类的方法的行为,这被称之为覆盖(overriding)那个方法。
如果继承只覆盖基类的方法(而并不添加在基类中没有的新方法),就意味着导出类和基类是完全相同的类型,因为它们具有完全相同的接口。结果可以用一个导出类对象来完全替代一个基类对象。这可以被视为纯粹替代,通常称之为替代原则。这种情况下的基类与导出类之间的关系称为is-a(是一个)关系,因为可以说”一个圆形就是一个几何形状”。判断是否继承,就是要确定是否可以用is-a来描述类之间的关系,并使之具有实际意义。
有时必须在导出类型中添加新的接口元素,这样也就扩展了接口。这个新的类型仍然可以替代基类,但是这种替代并不完美,因为基类无法访问新添加的方法。这种情况可以描述为is-like-a(像是一个)关系。新类型具有旧类型的接口,但是它还包含其他方法,所以不能说它们完全相同。
只使用纯粹替代的方式是很好的设计,但是实际上有些时候导出类还是会需要添加新的接口。
伴随多态的可互换对象
面向对象程序设计语言使用了后期绑定的概念。当向对象发送消息时,被调用的代码直到运行时才能确定。
把将导出类看做是它的基类的过程称为向上转型(upcasting)。代码只与基类交互,而和具体类型信息是分离的。
单根继承结构
除了C++以外的所有OOP语言都是单根继承。
容器
Java提供了List(用于存储序列),Map(也被称为关联数组,用来建立对象之间的关联),Set(每种对象类型只持有一个),以及诸如队列、树、堆栈等更多的构件。不同容器提供了不同类型的接口和外部行为;不同的容器对于某些操作具有不同的效率。例如ArrayList和LinkedList,选取元素时,LinkedList会比较低效,但是插入元素时,相对要高效。
如非确切地知道对象的类型,否则向下转型是不安全的。
Java SE5中加入了参数化类型,即泛型。
对象的创建和生命期
Java完全采用了动态内存分配方式,在被称为堆(heap)的内存池中动态地创建对象,堆带来了灵活性,但是存储分配需要更多时间。由垃圾回收器决定何时回收对象。
异常处理:处理错误
异常处理就像是与程序正常执行路径并行的、在错误发生时执行的另一条路径。因为它是另一条完全分离的执行路径,所以它不会干扰正常的执行代码。这往往使得代码编写变得简单,因为不需要被迫定期检查错误。此外,被抛出的异常不像方法返回的错误值和方法设置的用来表示错误条件的标志位那样可以被忽略。异常不能被忽略,所以它保证一定会在某处得到处理。
并发编程
隐患:共享资源。如果有多个并行任务都要访问同一项资源,那么就会出问题。对于共享资源,需要加锁。
一切都是对象
用引用操纵对象
尽管一切都看做对象,但是操纵的标识符实际上是对象的一个“引用”(reference)。String s;
这里创建的只是一个引用。
必须由你创建所有对象
存储到什么地方
程序运行时有五个地方可以存储数据:
- 寄存器:处理器内部,最快的存储区,数量极其有限
- 堆栈:位于通用RAM,速度仅次于寄存器,灵活性受限制,对象的引用,基本类型存于此处
- 堆:通用内存池,也位于RAM,灵活性大,分配与清理耗时,对象存储于此
- 常量存储:ROM中
- 非RAM存储:两个基本的例子是流对象和持久化对象。
特例:基本类型
基本类型的变量并非引用,直接存储“值”,并置于堆栈中。
Java中,没有无符号数,基本类型占用空间的大小也不会像其它语言随机器硬件架构变化。
Java 每种基本类型都提供了包装器类型。同时提供了两个用于高精度计算的类:BigInteger(任意精度的整数)和BigDecimal(任意精度的定点数)。
数组
Java中创建数组对象时,实际上就是创建引用数组(数组对象是由java虚拟机创建的),且每个引用都会自动被初始化为一个特定值(null)。编译器会把基本类型数组的内存全部置零。Java确保数组会初始化,且不能在范围外访问,但是增加了内存等开销。
永远不需要销毁对象
作用域
以花括号为界,与C、C++的区别:
1
2
3
4
5
6{
int x = 12;
{
int x = 96; //不合法,而在C、C++中外面花括号中的变量会被隐藏
}
}对象的作用域
Java对象不具备基本类型一样的生命周期,可以存活于作用域之外。
1
2
3{
String s = new String("a string");
}Java有一个垃圾回收器,用于监视new创建的所有对象,并辨别不再被引用的对象。
创建新的数据类型:类
字段和方法
在Java中,所做的全部工作就是:定义类、产生类对象、发送消息给类对象。类中有两种类型的元素:字段(或数据成员)和方法(或成员函数)。
当变量成为类的字段使用时,Java确保给定其默认值(内存中填0),以确保其得到初始化(C++无此功能),对于局部变量,不做初始化,可能是任意值。
方法、参数和返回值
方法名和参数列表(参数类型及参数顺序),合起来称为“方法签名”,唯一标识出某个方法。
关于方法的参数传递,这是一个被争议的话题。
先说说参数传递的几个术语:
值调用(call by vale):表示方法接收的是调用者传递的值。
引用调用(call by reference):表示方法接收的是调用者传递的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值;
JAVA语言总是采用值调用,也就是说,JAVA方法得到的是所有参数值的一个拷贝,方法不能修改传递给他的任何参数变量的内容。
例如:
1 | class StringAddress{ |
通过以上实例,可以看出基本类型参数传递的是变量值的拷贝,对象参数传递的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
很多程序语言提供两种传递方式:值传递和引用传递(C++和Pascal)。有些程序员认为java语言对对象参数传递也是用的引用调用。实际上是理解错误,这种错误具有一定的普遍性。
上例中swap方法并没有改变存储在变量sax和say中的对象引用。swap方法的参数a和b被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝。在方法结束时,参数变量a和b被丢弃了。原来的变量sax和say仍然引用这个方法调用之前所引用的对象。
这个过程说明:java语言对对象采用的不是引用调用,实际上对象引用进行的是值传递。
最后总结一下在JAVA中,方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的参数;
- 一个方法可以改变一个对象参数的状态(属性);
- 一个方法不能实现让对象参数引用一个新对象;
C++有值调用和引用调用。引用参数标有&符号。例如,void swap(int& a,int& b)。引用即可以当做变量的一个别名,仅此而已。而Java的引用,如果理解为像指针一样存的是对象地址,传递时都会把它拷贝一份,这样可能会好理解些。
构建一个Java程序
static关键字
对于static方法,不能简单地通过调用其它非static域或方法而没有指定某个命名对象,来直接访问非static域或方法。普通方法可以直接使用static域或方法。
使用类名是引用static变量的首选方式。
你的第一个Java程序
java.lang 包会自动导入到每个Java文件中。
一个独立运行的程序,文件中必须存在某个类与该文件同名,并且那个类必须包含一个名为main()的方法(事实上Java允许文件名和文件中的类不同命,但是类不能是public的)。static方法的一个重要用法是在不创建任何对象的前提下调用它,这一点对于main()方法特别重要,它是运行应用时的入口。
注释和嵌入式文档
javadoc
只能为public和protected成员进行文档注释。
不要在嵌入式html中使用标题标签
<h1><hr>
等。因为javadoc会插入这类标签,避免冲突。一些标签示例
@see:引用其他类
该标签允许用户引用其他类的文档
@see classname
{@link package.class#member label}
该标签与@see相似,只是它用于行内
{@docRoot}
该标签产生到文档根目录的相对路径,用于文档树页面的显式超链接
{@inheritDoc}
该标签从当前这个类的最直接的基类中继承相关文档到当前的文档注释中
@version
@author
@since
该标签允许你指定程序代码最早的使用版本
@param
该标签用于方法文档中
@return
@throws
@deprecated
该标签用于指出一些旧特性已由改进的新特性所取代,建议用户不要再使用这些旧特性,因为在不久的将来它们很可能会被删除
编码风格
类名的首字母大写;如果类名由几个单词构成,那么把它们并在一起,其中每个内部单词的首字母都采用大写形式。
几乎其他所有内容--方法、字段(成员变量)以及对象引用名称等,公认的风格与类的风格一样,只是标识符的第一个字母采用小写。
操作符
几乎所有的操作符都只能操作基本类型。例外的操作符是“=”,“==”,“!=”。另外String支持“+”和“+=”
静态导入
使用静态导入可以使代码简化,如下面代码不需要写Print.print(new Date());
1 | import static net.mindview.util.Print; |
赋值
基本类型赋值是直接将一个地方的内容复制到另一个地方。而“将一个对象赋值给另一个对象”实际是将“引用”从一个地方复制到另一个地方。
一元加
一元加号只是为了与一元减号相对应,唯一的作用仅仅是将较小类型的操作符提升为int。
关系操作符
对于对象==
,!=
比较的是引用。要比较对象的实际内容,应该使用equals()
方法。默认equals()
方法是比较引用(Object类中就是返回this==obj),所以应该在自己的类中覆盖此方法。Java不允许普通数字作为布尔值使用。
直接常量
直接常量后面的后缀字符标志了它的类型。
- 若为大写(或小写)的L,代表long。
- 若为大写(或小写)的F,代表float。
- 若为大写(或小写)的D,代表double。
对于long和float必须写出后缀字符。默认整型和浮点型是int和double。
Integer和Long类的静态方法toBinaryString()可以得到二进制字符串。
按位操作符
我们将布尔类型作为一种单比特值对待。可以进行按位与(&)、或(|)、异或(^)操作,但是不能进行按位非(~)操作。对于布尔值,按位操作符与逻辑操作符有相同的效果,只是不能中途“短路”。
移位操作符
- 左移位操作符(<<):按操作符右边指定数值移位后,低位补0 。
- “有符号”右移位操作符(>>):按操作符右边指定数值移位后,使用符号扩展,补齐高位,若符号为正,则在高位插入0,若符号为负,则插入1 。
- “无符号”右移位操作符(>>>):按操作符右边指定数值移位后,无论正负都在高位补0 。
char、byte、short类型的数值进行移位时,移位进行之前会先转为int,最后结果也是int型。只有右操作数的低5位才是有用的。这样可以防止我们移位超过int型值所具有的位数。没有任何移位操作符可以让一个数丢弃所有的位,int是32位。移位操作符只有右操作数低5位有效,实际是对右操作数对32取模后移位,50<<33
相当于50<<(33%32)
Java数字的二进制表示形式称为有符号的二进制补码
类型转换
窄化转换可能面临信息的丢失。
截尾和舍去
将float和double转型为整型值时,总是对该数值执行截尾。如果想要得到舍入结果,就需要使用java.lang.Math中的round()方法。
提升
对于基本类型执行算术运算或位运算,只要类型比int小(char、byte、short),在运算之前会自动转成int,最终结果也是int。复合赋值(+=、-=等)并不需要类型转换,尽管它们执行类型提升,但也会获得与直接算术运算相同的结果。
表达式中出现的最大的类型决定了表达式最终结果的数据类型。
控制执行流程
迭代
do-while和while唯一的区别就是do-while中的语句至少会执行一次,即便表达式第一次就被计算为false。while中,条件第一次为false,其中的语句根本不会执行。
for第一次迭代之前要进行初始化,随后会进行条件测试,每一次迭代结束时,进行某种形式的步进。
逗号操作符
注意是逗号操作符不是逗号分隔符(逗号用作分隔符时用来分隔函数的不同参数)。
Java里唯一用到逗号操作符的地方就是for循环的控制表达式。在控制表达式的初始化和步进控制部分,可以使用一系列由逗号分隔的语句,而且那些语句均会独立顺序执行。
通过使用逗号操作符,可以在for语句内定义多个变量,但是它们必须具有相同的类型。
例如:for(int i=1,j=i+10; i<5; i++,j=i*2)
break和continue
在java中,标签起作用的唯一的地方刚好是在迭代语句之前。“刚好之前”的意思表明,在标签和迭代之间置入任何语句都不好。在迭代之前设置标签的唯一理由:我们希望在其中嵌套另一个迭代或者一个开关。这是由于break和continue关键词通常只中断当前循环(最内层的循环),但若随同标签一起使用,它们就会中断循环,直到标签所在的地方。
- 一般的continue会退回最内层循环的开头(顶部),继续执行。
- 带标签的continue会到达标签的位置,重新进入紧接在那个标签后面的循环。
- 一般的break会中断并跳出当前循环。
- 带标签的break会中断并跳出标签所指的循环。
switch
switch现在支持整数(int、short、byte)、char、字符串、枚举类型。
初始化和清理
方法重载
区分重载方法
区分重载的规则:每个重载的方法必须有一个独一无二的参数类型列表。甚至参数顺序不同也足以区分两个方法,但是一般不要这么做,会使代码难以维护。
涉及基本类型的重载
如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int型。如果传入的参数较大,就得通过类型转换来执行窄化转换。
类类型的重载
调用哪个重载函数只和声明的类型相关,跟new出来的实例是哪个导出类无关。
SuperClass sub = new SubClass();
SubClass继承SuperClass,调用sub.fun()
,如果fun
被覆盖,执行子类的方法,其它情况均是匹配父类中的方法执行。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class A{
public void name(){
System.out.println("a");
}
public void name(Object o){
System.out.println("a");
}
}
class A{
public void name(){
System.out.println("b");
}
public void name(String o){
System.out.println("b");
}
}
public class Test{
public static void main(String[] args) {
A a = new A();
A b = new B();
a.name();
b.name();
a.name("a");
b.name("b");
}
} /* output:
a
b
a
a*/
默认构造器
如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
this关键字
编译器会暗自把“所操作对象的引用”作为第一个参数传递给方法。this关键词只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。
如果在方法内部调用同一类的另一个方法,不必使用this,直接调用即可。当方法需要返回当前对象的引用时才需要明确使用this,如需要返回当前对象时。
如果要将当前对象传递给外部的方法,this关键词就很有用。
可能为一个类写了多个构造器,想在一个构造其中调用另一个构造器,以避免重复代码。this关键字可以做到这一点。
尽管可以用this调用一个构造器,但不能调用两个。此外,必须将构造器置于最起始处,否则编译器会报错。
除了构造器外,编译器禁止在其他任何地方调用构造器。
static方法是属于类的,所以里面不能用this关键字。
清理:终结处理和垃圾回收
- 对象可能不被垃圾回收
- 垃圾回收并不等于“析构”
- 垃圾回收只与内存有关
finalize()
Java中finalize()的作用一主要是清理那些对象(并非使用new,比如调用了native方法)获得了一块“特殊”的内存区域。Java有垃圾回收器负责回收无用对象占据的内存资源。但也有特殊情况:假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存区域,为了应对这种情况,java允许在类中定义一个名为finalize()的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()的方法。并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。注意这里的finalize()并不是C++里的析构.在C++中,对象一定会被销毁,而在Java里的对象却并非总是被垃圾回收。
垃圾回收只与内存有关。也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们也必须同内存及其回收有关。
finalize()还有一个有趣的用法,它并不依赖于每次都要对finalize()进行调用,这就是对象终结条件的验证(中文版P88)。
成员初始化
Java尽力保证:所有变量在使用前都能得到恰当的初始化。类的每个基本类型数据成员保证都会有一个初始值。在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。
要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。
1 | public class Counter{ |
i,j的值首先都是0,之后才变成7,1。对于所有基本类型和对象引用,包括在定义时已经指定初值的变量,这种情况都是成立的。
顺序
在类的内部,变量定义的顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。
静态初始化只有在必要的时刻才会进行。只有在第一个对象被创建(或者第一次访问静态成员,其实构造器可以看成是静态方法)的时候,它们才会被初始化。此后,静态对象不会再次被初始化。
初始化的顺序是,先静态对象(如果他们尚未初始化,类加载过程中被初始化),而后是非静态对象(类实例化时被初始化)。
Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”)。此外还有实例初始化子句,这在匿名内部类初始化中是必须的(因为匿名内部类没有构造器)。
数组初始化
int[] a1;
这只是一个数组引用。
第一种初始化方法
java 1
2
3Integer[] a = new Integer[2];//对于非基本类型,此时只是创建了一个引用数组,必须对数组中的每个元素创建对象
a[0] = new Integer(1);
a[1] = new Integer(2);第二种初始化方法
java 1
Integer[] a = {1,2,};
第三种初始化方法
java 1
Integer[] a = new Integer[]{1,2,};
第二、三种方法中最后一个逗号是可选的。在方法调用的内部都可以使用第三种方式
1 | Other.main(new String[]{"abc","efg"}); |
可变参数列表
1 | public void name(Object... args); |
当指定参数时,编译器实际上会为你去填充数组。可以直接将数组传递给可变参数列表。0个参数的可变参数列表也是可以的。
1 | public void name(Integer... args){} |
上述代码重载了name方法,当使用name()
编译器无法知道调用哪个方法,无法编译。你应该总是只在重载方法的一个版本上使用可变参数列表,或者根本不使用
枚举类型
- toString():获得某个enum实例的名字
- ordinal():获得某个enum实例的声明顺序
- 静态方法values():用来按照enum常量的声明顺序,产生由这些常量值构成的数组。
你可以将enum当做任何类来处理,事实上enum确实是类,它具有自己的方法。