OOP
- 对象有三个要素
- behavior
- 接口是怎样的,有什么方法/field可以用?
- state
- 调用方法的时候,对象会有什么反应?
- 只有通过调用方法才能改变一个对象的state
- identity
- 对象之间如何区分?
- behavior
- 类的三大关系
- dependence
- uses-a
- 在方法里用到的类(方法的local variable)
- 应当尽可能地少
- Aggregation
- has-a
- 自己本身有的类(自己的field)
- Inheritance
- is-a
- general v.s. specialized
- dependence
Java自带类的入门
- 只封装了functionality的Math
- Date
- GregorianCalander
Java OO初步 - 以Date为例
- Date的API文档
- Date是用来表示某一个时刻而不是某一个现实世界的时刻的(设计的初衷是UTC时间,但是又没有遵守一些复杂的历法规则)
- 用来表示现实世界时间的另有一些其他类,比如GregorianCalander(常用的公历),这样方便不同的locale用不同的历法,它们都继承Calendar(相对于太general的Date而言,一个Calendar加上一堆子类确实是更好的设计……)
- derefernce一个null会在runtime抛出error
- 所有的Java对象都被放置在堆上
- 和JS类似,Java对象都是引用,=只会得到shallow copy,需要deep copy的时候需要调用Object均带有的clone()
Java OO初步 - 以GregorianCalander为例
- API
- 月份从0数起,为了防止出错以及减轻对底层实现的依赖,传月份的时候最好使用Calendar这个虚类的常量,比如Calendar.DECEMBER,星期的名字也是一样
- get和set需要将相关的field通过参数传入而不是直接调用不同名字的方法,相关的field用Calendar的常量表示 e.g. deadline.set(Calendar.YEAR, 2014)
- add可以修改时间,负数的增量表示倒退
- setTime和getTime可以用来完成Calander和Date之间的转换
- 有一个DateFormatSymbols用来获取当前locale关于日期的一些名字,比如星期一叫什么(Monday(英文)?星期一(中文)?)
自定义类
- main函数需要寄存在一个类里
- 一个文件里只能有一个public类,其他类可以有多个。文件名必须和自己的public类的名字相同
- Java编译器自带类似于make的功能,会查找依赖,如果没有找到.class会自动找对应的.java并编译,或者如果.java修改过了也会重新编译
- 构造函数要且必须要用new来调用,直接用会在编译时报错
- 虽然方法内声明的变量不存在shadow,但是方法参数的名字和类自带field的名字可以存在shadow(此时自带的field需要加上this),不过参数名不能再用来声明新的local变量
- this和JS里的用法一样,后面跟点
- Java里所有类的实现都和类的声明写在一块儿,长得类似C++的inline声明,只不过在Java里决定是否inline是编译器的活
- 在方法内返回类的field时,如果它是对象,Java不会做一个deep copy再返回,所以实际上返回的是这个对象的引用,会破坏封装。为此需要返回它的.clone()的返回值
Static
- Java里类中的常量使用也final声明,和C++不同的是,它在整个构造函数里都可以被修改(C++里常量一定要通过member initializer这种诡异的方法去设置,到函数体里就不能动了),出了构造函数才不能动
- Java标准库里那些标了native的函数是可以绕过Java自己的语法限制的,比如一个API里说是final的field还可以用某些方法修改(因为native的函数不是用Java实现的(通常是C/C++),所以可以绕过这个限制)
- Java的static方法和C++一样,没有this,不能调用非static的方法,也不能使用非static的field。同样地,static方法不一定只能用类名调用,也可以随意用一个这个类的对象调用,虽然这种做法比较confusing所以不推荐
- 为一个方法标明static大约有两种情形
- 这个方法不改变对象的状态,所以不需要修改nonstatic filed
- 这个方法只需要访问static的field
- 作者吐槽了一下static这个名字的三重含义2333 (C里面可以指只初始化一次的变量(最符合static字面意思的意义),或者只有当前文件能访问的变量。到了C++又加上了一个类里共有的成员的意思,也是java唯一沿用的含义)
- 在Java里,访问权限的设置和C++类似,同一个类对象可以访问彼此的private成员,子类不能访问父类的private成员,但是Java的继承只有public一种,没有那堆复杂无比的规则
- 顺带介绍了一下factory模式
-
main函数也必须是static的。注意每个类都可以有main函数,这样方便做单元测试。在运行的时候,只会用入口的.class里的main函数,其他类的main函数会被无视。这样的话,如果要跑一个叫ClassA的类的单元测试,可以在这个类里写个main函数,然后执行
$ java ClassA
如果要跑用了多个类,包括ClassA的程序,入口写在Runner.java里,可以跑
$ java Runner
这样ClassA的main函数就会被无视。注意这里可能还需要加上classpath说明怎么找到ClassA和其他类。另外,private类也可以有main函数,只要声明是
public static void main(String[] args)
就行了。
参数
- Java和JS一样,一切都是call-by-value,函数参数传进来的引用内容可以改,引用指向的对象不能改,原始类型不是引用就是直接copy再传进来了,怎么动都没事
对象的构造
- Java和C++一样,用signature实现函数重载
- 所有field都会有默认初识值(数字是0, boolean是false,对象是null)。注意是field才会有默认值,不初始化也能用,local变量是没有的,不初始化就不能用。
- 和C++一样,没有构造函数的时候会提供一个默认构造函数
- 在Java里用来给field直接赋值的构造函数参数名的几种convention是
- 用首字母,小写
- 开头加a,驼峰
- 直接同名,这时候会产生shadow,所以在指定field的时候要加this
- 没有C++那种加下划线的convention
-
Java里构造函数居然可以互相调用 =口= 用法是
this(...)
然后就会根据传进去的参数匹配对应的构造函数。这个调用一定要写在构造函数的第一句。这样一来如果很多构造函数都有相同的代码,就可以抽象到某一个构造函数里,然后大家一起调用它。
- 可以在类声明里直接给field写上初始值,这个初始化可以在构造函数前执行完,所以如果有一些field需要一个很多构造函数都会给它的初始值,可以在类声明里面写,就不用在N个构造函数里都写了。注意这个初始值还可以是某个函数的返回值,不一定需要是literal。
- 如果光用一个函数返回值还不足以初始化,那么还可以用上initialization block,里面就可以写语句了,参见Initializing Fields (The Java™ Tutorials > Learning the Java Language > Classes and Objects)
- initialization block和field的声明不一定有先后,可以夹杂起来的,而且没有声明的field还是不能初始化的,这里有一堆复杂的规定(复杂到早期的java编译器自己都有些不符合speciation的地方),一般来说建议把所有声明都写完再写initialization block
- 初始化的顺序大约是
- 先赋默认值
- field initializer和initialization block按照在代码里出现的顺序执行
- 如果这个构造函数还调用了另一个构造函数(人家要写在第一行),调用它
- 最后执行这个构造函数的函数体
- static field还可以通过一个static initialization block初始化,只要在花括号前写上static就行。
- static initialization block会在这个类第一次load的时候执行,所以如果你在里面写上输出hello world的语句,你就可以在没有main函数的情况下输出hello world = =虽然运行时会会在执行完static initialization block里的语句后报错main函数没有定义。如果你还想逃脱这个报错,只要加上System.exit(0)就好(这样玩真的有意思吗喂!!)
对象的析构
- Java里没有析构函数,但是可以有一个叫finalize的方法,在每次GC清理对象的时候会执行。不过这个方法执行的时机是不确定的,所以不能依赖它回收资源。最好的方法就是直接在任何申请了资源的函数里及时回收。
- Java的GC和JS差不多,用的是Mark and Sweep,所以无法确定finalize何时被调用
package
- java的类可以放在package里分发,package的命名方式通常会按照所属机构的域名倒着写,所以你会经常看到叫com.xxx.yyy的pacakage,制作这个package的人所在的机构的网站域名就会是xxx.com
- 嵌套的package没有包含关系,java.util和java.util.jar里的类是不一样的。
- 调用一个package的类可以每次指明它都写上全名,也可以先import它进来就不用写前面的package的名字了。import一定要写在文件开头。
- import可以用 * 来获得一个package的所有类,eclipse提供将 * 展开成文件里使用过的类名的功能。注意 * 只能用来指一个package的所有类,不可以放在上一级指多个package
- bytecode里会把所有类名展开全,java编译器和C++编译器的区别注意在于C++的编译器不会看被#inlcude的文件的内容,而java会看被import的类的代码
- import还可以用来获得一个类的public static的方法或者field,这时需要写类似import static package.class.method的语句(注意import后跟static)。如果想import全部static成员,用 * 就行。
-
如果想将一个新的类加入现存的package里,可以在这个类的声明前写上
package packageName
在指定的package里将这个类塞进去。如果不写,这个类就会放进default package里
- 放在package里的类编译得到的bytecode需要按照package的名字创建好目录结构(每个点分割的元素加一层文件夹),放在项目根目录下。这样JVM就知道去哪里找这个类的bytecode(这里假设根目录会加进classpath里)。
- 这个package的查找是在运行时进行的,编译器不会去检查,JVM才会。
- 一个类里可以引用同一个package的其他类和其他package的public类。注意类名前不写modifier默认是private,但是类的field前不写默认是public(和C++相反)。很多人会忘记写上private,于是同一个package的所有类都可以随便获得这个field了,Java标准库里的java.awt.Window的warningString就是这样一个field呵呵呵呵,而且这个bug一直没被解决(作者自黑了一把:我们在core java第八版都说过这个bug了他们还没改,显然java标准库的维护者们都不看我们的书呢2333)
- 对于存在这种笔误的field,你可以通过设置文件目录+使用package来偷偷修改掉这个field,比如利用前面提到的标准库里的bug,你可以突破封装,修改标准库里的warningString 23333 不过从JDK 1.2开始所有java.开头的package都被seal起来不能用package改了,算是亡羊补牢。
class path
- .jar其实也是一种ZIP格式,所以能够打开ZIP的软件也能用来打开或者解压.jar。里面通常是一个按照package的名字构造好的目录结构,里面按规则放着.class和其他文件,这样可以省空间。
-
JVM寻找类的时候会从class path里列出来的目录开始按package的名字结构去找。class path可以在编译指令里用-classpath(缩写-cp)设置,也可以在环境变量CLASS_PATH里设置一个通用的列表
-
注意当运行指令里没有-classpath的时候,默认的class path包括运行时的工作目录。但是如果有设class path,工作目录是不会默认出现在里面的,此时有两种解决方案
-
在class path里直接加上工作目录,比如在Unix-like系统下
$ java -cp .:dir1:dir2:dir3:dir4... className
开头的点就表示工作目录,Unix-like系统下用冒号分割(在win下用分号)
- 在环境变量CLASS_PATH里直接加上那个点,就不用每次写编译指令都设进去了
-
- 注意随便设环境变量不是好事,而且一旦指明了CLASS_PATH又不在里面加上点,每次跑的时候class path总会缺了当前目录——苹果的QuickTime installer曾经干的好事
- 编译器(javac)总是会检查当前目录的,不会检查的是运行时(java)
- 注意class path里的wildcard用法和linux下的不同,* 不能随便乱用,参见 Java SE Documentation: Setting the class path
-
- runtime库里的文件和jar会自动加进class path
-
JVM的查找顺序是
- 在runtime库里查找(比如jre/lib)
- 在class path里嵌套的package里找
- 从当前目录开始查找
- 从class path里指明的jar里查找
-
Java编译器的查找
- 编译器会参考所有import语句来搜索
- 编译器会以class path里指明的每一个目录为起点,展开import语句去搜索
- 编译器找到更新过的代码也会自动重新编译
- 当找到多个重名的类的时候会报编译时错误
- 因为一个文件只能有一个public类,而且文件名还要和类名一样,所以编译器查找文件的时候会方便很多。但是如果在同一个package里,一个类是可以import同一package的其他private类的,这个时候要靠编译器去把这个package里的文件都看一遍才行。
- 所以Java编译器的查找虽然不那么需要在class path里写明一堆目录,但是慢很多
注释
- Java有一个很吊的工具,叫做javadoc,写法参见这里和这里。只要按照约定好的方式去写注释,用javadoc跑一遍就能自动生成HTML文档。JDK6的官方API文档和hadoop的官方API文档都是用这个生成的。
- JDK6生成的样式长得太蛋疼了,自己魔改了一个css在这里。好在javadoc生成的文档会共用目录下的一个stylesheet.css,通过修改它就可以影响这一套文档的外观。
Before
After
一些tips
- 当一个类里有太多个基本类型的field的时候,说明可能有一些field本身就可以组成一个类然后再加进来了。