02.java面向对象设计
02.java面向对象设计
2.1 面向对象设计
- 面向对象设计也称oop,通过先定义数据再考虑操作数据的算法解决规模较大的问题
- 通过使用对象来存储数据,oop设计要先弄清楚对象的行为(要完成的操作),状态(进行操作时,对象如何响应),标识(如何区分不同状态的对象)
- 在创建类的时候,确定其中哪些名词重要,通过经验识别类
- 类之间的关系:依赖(uses-a),聚合(has-a),继承(is-a)
- 依赖:如果一个类方法使用或操作另一个类,称该类依赖另一个类(尽量降低)
- 聚合:意味着包含另一个类的对象(类中有另一个类的变量)
- 继承:由类继承得来
- 面向对象的特点:封装、继承、多态
- 封装:类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
- 继承:继承类可以使用基类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
- 多态:允许不同继承类对基类的方法有不同的运作方式(允许将子类类型的指针赋值给父类类型的指针,使用父类中的方法得到不同效果)实现多态,有二种方式,覆盖,重载。覆盖,是指子类重新定义父类的虚函数的做法;重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)
2.2 类的定义和导入
- 类的定义:使用
class语句
class ClassName
{
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}这些类可以在多个源文件中,在运行时,只需要使用
javac main.java就可以完成多文件的编译,它会自动寻找要使用其他类的源文件,编译或更新,java中类都放入了包中类的导入:使用
import 包名;可导入类(需给出完全限定名),例:java.util.ArrayList;或java.util.*;(这样java在编译时会寻找要加载的类,不会将所有类都加载),但是为了方便了解使用了哪些包,建议写全名- 当出现多个包内都有同名类,可以在之后再使用import 类解决,如果两个类都要用,使用时使用完整的包名
静态导入:使用
import static 包名;可导入该包中类的所有静态方法,使用时就不用在方法前使用类名.在包中增加类(创建包):使用
package 子目录路径;就可以将类方入包中,在使用时,直接切换到基目录,编译主文件即可,如果路径有问题,能编译不能运行,使用package命令,说明运行cmd的目录下应该有该子目录,在基目录运行子目录的包时,应该使用文件夹.文件名运行,且只能在基目录运行它
一般命名包方法是互联网连接命名,PackTest.test.com等,反过来就是根目录/com/test/PackTest.java,而添加类时,使用package com.test;
- 无名包:如果没有在java源文件中使用package语句,默认将所有的方法放入无名包中
- 可执行JAR文件:可以通过jar命令在命令行生成jar包,在java9以后允许对不同版本的java使用多版本jar包,在这里不展示,详情点这里
2.3 类中的属性和方法
- java中类属性和方法不像C++中一样分为
private/public/protected,java不止使用这些,还有不带修饰符,强烈建议所有类属性不使用public,封装类,修饰符作用如下:

修饰符:
- static:将属性或方法设置为静态,可设默认值,可以使public方法或属性可以不用使用对象访问,可以使用类名访问,例如Math.pi得到π,使用Math.pow(2,2)得到,静态属性和方法为所有的对象公有,只有一份
- final:同前面的final,声明一个常量,必须在构造时初始化,之后无法修改
- 构造函数:和C++一样需要使用和类同名的方法定义,可重载,仅当未定义构造函数时,默认给出无参构造函数,且不像C++,java无参数列表赋值,但同样具有隐式参数(类属性,也可以使用
this.访问),java允许在定义时给参数赋值- 默认无参构造函数会把数字设置为0,boolean为false,其它对象为null,自定义构造函数中没有定义的初始值也将设置如此(但建议写出)
- 在java中在构造器的定义中允许使用this(参数)调用其它构造器,C++不能如此
- 初始化块:使用构造器构造对象时,会触发初始化块,调用顺序如下:如果构造器第一行调用了另一个构造器,则先执行第二个构造器,否则先初始化默认值,再执行初始化块,最后执行该构造器代码,初始化块就是仅用大括号包裹的函数
- 静态初始化块:如果使用
static修饰初始化块,就可以得到静态初始化块,只会在第一个对象初始化时使用,用于对需复杂初始化的静态字段初始化 - 工厂方法:如果需要生成不同风格的初始化对象,可以使用工厂方法,一般工厂方法由多个静态方法组成,例如LocalDate和NumberFormat类使用静态工厂方法构造对象,通过工厂方法和继承配合使用,就可以得到不同的效果,有关为什么不使用构造器:1️⃣由于构造器命名必须与类名一致,这里希望有两个不同的名字加以区分,2️⃣使用构造器不能改变构造的类型,这里实际上是通过继承,将其变成了不同功能的子类
//工厂方法构造不同风格的对象
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x)); //print $0.10
System.out.println(percentFormatter.format(x)); //print 10%- 析构函数和finalize方法
- 不同于C++,在java中不支持析构器,java会自动进行垃圾回收
- 如果使用了除内存以外的资源,如:打开了文件或连接等,使用完成需要立刻关闭,需要定义一个close方法完成必要的清理工作
- finalize方法在目前版本已废弃,在之前会在回收之前自动调用,不要使用
class Employee{
//类中属性和方法演示
public static int nextId = 1;
/*如果使用随机初始值,写法
private static int nextId;
//静态初始化块,,只会执行一次
static
{
//使用var如果根据根据后方构造函数辨别该类,可以使用var定义,java10特性
var generator = new Random();
nextId = generator.nextInt(10000);
}
*/
private int id;
private String name;
private double salary;
//初始化块,不写也可以,虽然可以出现在任何地方,但建议写在定义变量之后
{
//初始化时自动执行
id = nextId++;
}
//构造函数
public Employee(){
//可直接访问,但注意不要定义同名变量
name = "";
salary = 5000;
}
public Employee(String n,int s){
//使用this访问
//调用其它构造器
this();
this.salary += 1000;
}
}2.4 类的设计思路
- 一般情况下,如果想要得到或修改字段的值,类需要提供:
- 一个私有数据字段
- 一个对应的访问器方法
- 一个对应的修改器方法
一般情况下,C++会使用const声明函数不会修改类属性,只会访问类属性,而java没有这种声明
- 字段:一般情况下,将有相同作用的字段合成成另一个类,使用时定义这个类以降低耦合度
- 访问器方法:一般设计为公有,一般的基本类型,这个可以直接
return 值;一句话结束,而对于其他类对象而言,不能直接返回,因为他们相当于C++指针,会指向一片区域(但是在传递给局部变量时,局部变量指向的修改不会影响本身),会导致出现外界访问修改字段,影响类的封装,因此,需要return 类对象.clone(); - 修改器方法:在没提继承前,修改器方法和普通函数一样,存储算法,另外要注意java无友元函数
c++中使用friend定义的函数就是友元函数,定义时不用使用类名::函数,而是像普通函数使用函数名定义,但是在java中没有,但java可以模拟出来 1️⃣友元类:在类内定义内部类可访问外部类的所有成员域和成员方法。注意这个内部类应该是非静态的(没有static关键字) 2️⃣友元函数:Java中我们也可以通过把某个成员域或者成员方法的访问权限设置为default类型(即不加任何访问修饰符),这时该成员域或者成员方法只能被当前package下的类访问,连子类都不能访问(子类需要得话,使用protected,但这种保护性差)
- 设计技巧: 1️⃣一定要保证数据私有,绝对不要破坏类的封装性 2️⃣一定要对数据进行初始化,最好不要使用系统默认值,且显式初始化所有数据 3️⃣不要在类中使用过多的基本类型,用其他类代替多个相关的基本类型,便于修改理解 4️⃣不是所有字段都需要单独的访问器和修改器,有些对象常包含不希望被获得的字段 5️⃣分解有过多职能的类,但不要走极端
2.5 java类文档的注释编写
- 在jdk中包含很有用的工具,叫做javadoc,它可以由源文件生成一个html文档
- 读取的注释:javadoc从下面几项抽取信息,这里主要介绍类的部分即后三个
- 模块
- 包
- 公共类和接口
- 公共和受保护的字段
- 公共和受保护的构造器及方法
- 类注释:写于import语句之后,类定义之前使用文档注释/** 注释内容 */,其他行的*可不写,大部分编辑器也会给出
/**
* A {@code Card} object represents a playing card , such
* as "Queen of Hearts". A card has a suit (Diamond , Heart ,
* Spade or Club) and a value (1 = Ace , 2 . . . 10, 11 = Jack,
* 12 = Queen , 13 = King).
*/
public class Card{
...
}- 方法注释:必须放于被描述的方法前,可以使用通用标记和下方标记
- @param variable description:给当前方法的参数(parameters)添加条目,可占多行,可使用html标记
- @return description:给当前方法添加返回(return)部分,可占多行,可使用html标记
- @throws class description:给方法添加注释,表名方法可能抛出异常
/**
* Raises the salary of an employee.
* @param byPercent the percentage by which to raise the salary (e.g. 10 means 10%)
* @return the amount of the raise
*/
public double raiseSalry ary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
return raise;
}- 字段注释:只需对公共字段(通常指的是静态常量)建立文档
/**
* The "Hearts" card suit
*/
public static final int HEARTS = 1;- 通用注释:
- @since text:建立一个始于(since)条目,text可以是引入这个特性的版本的任何描述
- @author name:产生作者(author)条目,可有多个,对应多个作者
- @version text:产生版本(version)条目,text可以是对版本的任何描述
- @see 和 @link标记,链接到javadoc文档的相关部分或外部文档,标记@see reference将在“ see also” 部分增加一个超级链接。它可以用于类中,也可以用于方法中。这里的引用可以选择下列情形之一:
package.class#feature label<a href="...">label</a>"text"而@link可以在任何位置添加指向其他方法和类的超链接,方法规则和@see标记相同 - {@index entry}语法在java9中能被使用,可以为搜索增加一个条目
//@since用法
@since 1.7.1
//@author用法
@author James Gosling
//@version用法
@version This version is a beta version
//@see用法
//第一种情况是最常见的,只要提供类、方法或变量的名字,javadoc就在文档中插入一个超链接。例如
@see com.horstraann.corejava.Employee#raiseSalary(double)
/*建立一个链接到 com.horstmann.corejava.Employee 类的
raiseSalary(double) 方法的超链接。
可以省略包名, 甚至把包名和类名都省去,此时,链接将定位于当前包或当前类
需要注意,一定要使用井号(#) 而不要使用句号(.)分隔类名与方法名,或类
名与变量名。Java 编译器本身可以熟练地断定句点在分隔包、子包、类、内部类与方
法和变量时的不同含义。但是 javadoc 实用程序就没有这么聪明了,因此必须对它提
供帮助*/
//如果 @see 标记后面有一个<字符,就需要指定一个超链接。可以超链接到任何URL,例如:
@see <a href="m«w.horstmann.com/corejava.html ">The Core Java home page</a>
/*在上述各种情况下, 都可以指定一个可选的标签( label ) 作为链接锚(link
anchor)如果省略了 label , 用户看到的锚就是目标代码名或 URL。
如果@see 标记后面有一个双引号(")字符,文本就会显示在 “see also” 部分,例如,*/
@see "Core Java 2 volume 2"
//可以为一个特性添加多个 @see 标记,但必须将它们放在一起
//@link用法
{@link package.class#feature label}- 包注释
- 直接将类、方法、变量的注释写在java源文件中,只要用/** */注释界定就可以了,想要产生包注释,需要在每一个包目录中添加一个单独文件
- 添加方法有以下两种: 1️⃣ 提供一个
package-info.java文件,该文件必须包含一个初始的以/** */界定的javadoc注释,后面是一个package语句,它不能包含更多的代码或注释 2️⃣ 提供一个名为package.html的文件,会抽取标记<body></body>之间的所有文本
- 注释抽取的方法 假设你希望html文件将放在docDirectory的目录下,执行以下步骤: 1️⃣ 切换到想要生成文档的源文档的目录。如果有嵌套的包要生成文档,例如,com.hostmann.corejava,就必须切换到包含子目录com的目录(回到最外层的目录,如果提供了overview.html的话,就是这个文件所在的目录) 2️⃣ 如果是一个包,运行javadoc -d docDirectory nameOfPackage 或者,如果是多个包,运行javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ... 3️⃣ 如果是无名包,运行javadoc -d docDirectory *.java
如果省略了-d docDirectory选项,那么HTML文件将会被提取到当前目录下,有可能带来混乱,因此不提倡这种做法
- javadoc其它选项:
- -author和-version可以使用来在文档中包含@author和@version标记(默认会忽略)
- -link可以连接其他超链接的标准类注释文档
javadoc -link http://docs.oracle.com/javase/16/docs/api *.java使所有的标准类库自动连接到oracle官网 - -linksource可以使每个源文件转化为HTML(不代码着色,但包含行号),并且每个类和方法名将变为指向源代码的超链接
2.6 类的继承
- 定义子类:定义子类使用exends关键字
- java不支持多继承,即 一次继承extends后面只能跟一个类,如果实在需要多重继承.,需继承多次,而且接口有类似多重继承的功能
//B类与A类为继承(is-a)关系
//A是B的基类、超类、父类
//B是A的派生类、子类、孩子类
class B extends A{
}在C++中,存在public、private等多种继承方法,但在java中只有public继承,C++的继承方式会把基类的公有属性和方法变成public、private等,因此,java为了阻止部分公有方法的继承,可以重写或隐藏这些方法
- super关键字
- 有的时候新定义的方法需要访问基类的公有方法,java不存在基类名::方法名调用基类方法,这时,就需要和this对应的super了。如果需要覆盖超类的方法完成对超类数据和新增数据的修改,就可以使用这种方法
- 子类构造器:对于初始化基类的字段,由于派生类没有访问修改的基类字段的方法,也必须使用基类的初始化器super()
//使用super的方法重写覆盖
public double getSalary(){
return super.getSalary() + bonus;
}
//使用super的构造器
public Manager(String name,double salary,int year,int month,int day){
super(name,salary,year,month,day);
bonus = 0;
}- 将对象放入基类的数组中
- 例如,将Manager类的对象赋值给Employee类对象中,没问题,使用
Employee对象名.getSalary()时,得到的是Manager的getSalary()方法的结果 - 这种能指示多种实例类型的现象称为多态,在运行时能够自动地选择适当的方法这称为动态绑定(dynamic binding)
- 例如,将Manager类的对象赋值给Employee类对象中,没问题,使用
在C++中,如果希望实现动态绑定,需要将成员函数声明为virtual(虚)。在java中,动态绑定是默认行为。如果不希望动态绑定,可以使用final标记,阻止继承
2.7 多态
- 引入:有一个简单的规则来判断是否应该设计继承关系,那就是"is-a"规则,它指出子类的每一个对象都是超类的对象(或者说,子类对象出现的地方可以在逻辑上使用超类对象替换)。在java中,继承不仅限于一次,例如:Employee类可以引用Employee类型的对象,也可以引用一个子类的对象(例如:Manager、Executive、Secretary等),这样,超类只能访问自身拥有的方法和字段,如果使用超类的方法,对于不同的类型产生了不同的效果,就是多态的一种体现
警告:如果使用超类的数组存储子类的变量,这是合法的,但是如果未被记录存放的是子类的变量,被修改为存放超类对象之后,使用子类独有的方法会出错,调用一个不存在的字段,进而搅乱相邻的存储内容,不建议这样存储;且一定要注意,仅将兼容的数据存放在一起,不然会引发ArrayStoreEception异常
final阻止继承:使用final标记的方法和类将不允许被扩展,将类声明为final,其中有方法也自动成为final方法
方法参数也可以使用final标记,意为不可修改
//不允许扩展(这里指无法继承)的类 public final class Excutive extends Manager{ ... } //不允许扩展(这里指无法重写)的方法 public final String getName(){ ... }强制类型转换:对继承链上的类进行类型转换时,需要先进行判断,再进行操作,查看能否进行转换,否则如果承诺过多(谎报对象包含的内容,承诺它为...类型),程序将产生ClassCastException异常,终止
- 判断方法:instanceof关键字,如果可以转换返回true,否则返回false
- 综上所述,只能在继承层次内进行强转换,在将超类转换为子类时,应该使用instanceof检查
- 尽量不要使用强制类型转换,如果一定要使用基类的对象调用派生类特有的方法请思考这种设计是否合理
java使用强制类型转换语法来自于C++以往糟糕的日子,但处理过程却有些像 C++的
dynamic_cast操作。例如,Manager boss = (Manager) staff[1]; // Java等价于Manager* boss = dynamic_cast<Manager*>(staff[1]); // C++它们之间只有一点重要的区别: 当类型转换失败时,Java不会生成一个null对象,而是抛出一个异常。从这个意义上讲,有点像 C++ 中的引用(reference)转换。真是令人生厌。在 C++ 中, 可以在一个操作中完成类型测试和类型转换。Manager* boss = dynainic_ca.st<Manager*>(staff[1]).; // C++if (boss != NULL)...而在 Java 中, 需要将 instanceof 运算符和类型转换组合起来使用:if(staff[1] instanceof Manager){ Manager boss = (Manager) staff[1];}
- 方法调用理解(假设要调用x.f(args),隐式参数x声明为c类的一个对象): 1️⃣ 编译器首先会查看对象的声明类型和方法名,在c类寻找与需要的方法名相同的方法(已知所有候选方法) 2️⃣ 接下来,确定方法调用提供的参数类型(被称为重载解析),如果存在完全匹配就选那个方法,不然会继续类型转换(int可转换为double,子类可以转换为基类),如果没有匹配或有多个匹配,报告错误(已知需要调用的方法和参数类型) 3️⃣ 如果有特殊修饰符,编译器将可以更准确知道应该调用哪个方法(被称为静态绑定),如果要调用的方法依赖于隐式参数的实际类型,编译器会动态绑定生成调用f(String)的指令 4️⃣ 在运行并且使用动态绑定时,虚拟机必须调用与x所引用对象的实际类型对应的方法,假设x的实际类型是D,它是c类的子类,如果类D定义了这个方法,就会调用这个方法,否则会在D的超类寻找这个方法,以此类推
java中,方法的名字和参数列表被称为方法的签名,而返回值不是,不过在覆盖一个基类方法时,允许使用子类,将返回值修改为返回值的子类,且覆盖方法时,新方法不能低于超类方法的可见性,超类声明为public覆盖时也必须声明为public
- 如果已知的后续方法只有一个,将不在进行下一步
- 每次调用要如此寻找时间开销相当大,因此虚拟机预先为每一个类计算了一个方法表,其中列出了所有方法的签名和要调用的实际方法,真正在调用时,虚拟机只要查这张表就可以
- 抽象类
- 如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。例如,考虑一下对Employee类层次的扩展。一名雇员是一个人,一名学生也是一个人
- 每一个人都有一些属性,例如:姓名,因此通过引入一个公共的超类,把
getName()方法放在继承层次更高的一层,这样可以更好地实现多态,使用超类指向任何子类对象 - abstract关键字:在person类中有许多方法已知在派生类中会出现并重写,如果在person中定义,可以不用实现这些方法,这时,可以直接返回一个结果,更好地方法是使用abstract修饰,
public abstract getDescription();,为了提高代码的清晰度,包括一个或多个抽象方法的类也必须用abstract修饰 - 抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了
- 需要注意,可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象
在C++中,有一种在尾部用= 0标记的抽象方法,称为纯虚函数,例如:class Person {public:virtual string getDescriptionO = 0;}// C++只要有一个纯虚函数,这个类就是抽象类。在C++中,没有提供用于表示抽象类的特殊关键字
- 受保护访问(谨慎使用):
- 如果想要在子类使用超类的字段,就可以使用protected允许本包和所有子类访问
- 谨慎使用,你不知道其他程序员是否会定义新的派生类,从而泄露字段
- 受保护的方法反而更常用,这个修饰表明子类能很好得使用祖先类的这个方法,得到了信任,例如:object的clone方法(但是,受保护访问比较微妙,类只能调用它来克隆自己的对象,在java中,保护字段只能由同一个类访问,保护方法只能对同类对象使用,对超类无用)
- 修饰符总结在这里
2.8 所有类的超类Object类
- 和python一样,在java中有所有类的超类,java中所有类都扩展了object,但不要特别写:extends Object,只要没有明确指出超类,Object就被认为是这个类的超类,因此熟悉这个类很重要
- 认识Object:
- Object的变量可以引用任何类变量对象,在java中只有基本类型变量不是类对象,数组也扩展了Object类
equals(Object类)方法由于检查两个对象是否相等,object中实现的是默认行为:如果两个对象引用相等,那么他们就相等(但在实际中有可能只要比较部分是否相等,就可以了),而使用Objects.equals(a,b)也可以比较a和b是否相同,如果a,b中有一个为null返回false
如果两个变量不属于同一个类,equals应该如何操作存在争议,许多人喜欢使用instanceof检查(这样会允许类为子类时返回true),但是这样可能会出现一些麻烦,java语言规范要求equals具有自反性(x.equals(x) = true),对称性(x.equals(y) = y.equals(x)),传递性,一致性(x和y不变,结果不变) 因此,现在来看,有两种不同的情形: 1️⃣ 子类有自己相等的概念,应该不使用instanceof判断类型,而使用getClass()方法 2️⃣ 如果由超类决定是否相等,那么可以使用instanceof,这样可以在不同的子类间判断
⚠️ 另外注意重写的方法参数也应该为Object对象,否则无法覆盖,可以在前面声明 @Override确保覆盖,如果@Override public boolean equals(Object other) 没有覆盖超类的方法,将报告错误
hashCode()方法得到对应的散列码(hash code),一个整数(由对像的存储空间得出,相等的对象会返回一样的散列码,如果x.equals(y) = true,则x.hashCode() = y.hashCode(),二者必须相容(互相验证),如果覆盖了也必须如此)toString()返回一个输出所有结果的字符串
2.9 泛型数组
- 在某些语言中,必须在编译时确定数组的大小,很令人反感,java允许在运行时确定数组的大小,解决这个问题的方法是使用java类ArrayList,ArrayList类类似数组,在添加和删除数组元素时,会自动调整数组容量,不需要编写任何代码
- 声明数组列表:以Employee类数组列表为例,
ArrayList<Employee> staff = new ArrayList<Employee>();//java 10以下版本var staff = new ArrayList<Employee>();//java 10,可在括号内指定容量 - 允许使用for-each迭代
- ArrayList的方法
.add(num)方法:添加元素num到数组尾
.ensureCapacity(int)方法:估计出存储的元素数量,在填充前调用,就不用调用很大的开销来重新分配空间
.size()返回当前列表包含的实际元素个数
.trimToSize()确定空间不再变化,回收未使用的空间,在确认不会在添加后使用
.set(int i,num)方法在i位置上赋值元素(i必须<.size()),等价于数组中a[i] = num;
.get(int)方法得到在i位置上的元素,等价于数组a[i],不能使用a[i]访问
.toArray(int[])方法将数组元素拷贝到int数组中
.add(n,e)方法在位置n插入一个元素,位置n以及以后元素后移
.remove(n)方法删除n位置元素,之后的元素前移,.size()减一- 类型化和原始数组列表的兼容性:对于使用ArrayList作为参数的遗留函数,例如:
int f(ArrayList a)可以将一个类型化的ArrayList<Employee>的参数传递给f(),而不需要强制类型转换,这样调用可能不太安全,但虚拟机的完整性没受到威胁,相反则会得到一个警告,强制类型转换之后赋值也不行,如果使用命令行运行,希望看到这些警告信息编译时,提供选项-Xlint:unchecked - 对象包装器和自动装箱:
- 有时需要将int类型这样的基本类型转换为对象,所有的基本类型都有对应的类,例如:
integer类对应int类型(数字基本类型对应的六个类派生于Number超类),包装器类不可变,一旦赋值无法更改,且包装器类都用final修饰 - 在添加元素时,调用
list.add(3)会自动变换成list.add(Integer.valueOf(3)),这是很有用的特性,从而很容易地添加元素,这种变换称为自动装箱,装箱一词源于C#,将一个Integer对象赋值给一个int类型也会自动拆箱 - Integer是一个类对象,因此对两个存储值相等的Integer对象使用
==号判断,通常会失败,也有可能成功,比较时请调用equals方法;因为是类对象,会存在null赋值,要考虑这种情况,避免出现null拆箱,引起NullPointerException错误 - 在Integer类这样的包装器类中存放有一些重要的静态方法,可以完成对字符串的转换
- 有时需要将int类型这样的基本类型转换为对象,所有的基本类型都有对应的类,例如:
int intValue( )
//以int的形式返回Integer对象的值(在 Number 类中覆盖了 intValue方法)
static String toString(int i )
//以一个新String对象的形式返回给定数值i的十进制表示。
static String toString(int i ,int radix )
//返回数值i的基于给定radix参数进制的表示。
static int parselnt(String s)
static int parseInt(String s,int radix)
//返回字符串s表示的整型数值,给定字符串表示的是十进制的整数(第一种方法)
//或者是radix参数进制的整数(第二种方法)
static Integer valueOf(String s);
static Integer valueOf(String s, int radix);
//返回用s表示的整型数值进行初始化后的一个新Integer对象,给定字符串表示的是
//十进制的整数(第一种方法)或者是radix参数进制的整数(第二种方法)这种转换执行效率低下,只有当认为方便性比执行效率更加重要时才会使用
2.10 可变参数数量的方法
- 一些方法被称为变参的方法,例如
printf()方法
//printf方法是这样定义的:
public class PrintStream
{
public PrintStream printf(String fmt , Object... args) { return format(fmt, args); }
}- 在上面出现的
...是java代码的一部分,这会使args传入数组形式的参数new Object[]{};同时也允许直接传入数组形式的参数 - 因此如果最后一个参数是数组参数,可以这样写
2.11 枚举类
- 事实上,枚举类型声明的是一个类,定义典型例子:
public enum size{SMALL,MEDIUM,LARGE,EXTRA_LARGE} - 如上:它有四个实例,不可能构造新的对象,在比较时直接使用
==就可以了,如果需要的话,可以增加新的方法:
public enum Size
{
SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
private String abbreviation;
//枚举类的构造器必须为私有,否则会出错
private Size(String abbreviation) { this.abbreviation = abbreviation; }
public String getAbbreviation() { return abbreviation; }
}- 枚举类的方法:
- 设置s为上方
Size类的SMALL,Size s = Enum.valueOf(Size.class,"SMALL") - 返回"SMALL"字符串
Size.SMALL.toString() - 返回"MEDIUM"计数位置(从零开始)
Size.MEDIUM.ordinal() - 返回包含所有枚举值的数组
Size[] values = Size.values();
- 设置s为上方
Enum类实际上有一个类型参数,例如:Size实际上扩展了Enum<Size>,这个类型参数会在进行compareTo(other)方法中使用,比较枚举的量的出现顺序,如果本身在other出现前返回负整数,this == other返回0,否则返回正数
2.12 反射库
能够分析类能力的程序称为反射。使用反射,JAVA可以支持用户界面生成器、对象关系映射器以及很多其他需要动态查找的开发工具
反射机制可以用来:
- 运行时分析类
- 运行时检查对象,例如:一个适用于所有类的toString()方法
- 实现泛型数组操作代码
- 利用Method对象,这个对象类似C++的函数指针
一般应用的程序员不需要考虑这个,开发工具可以使用,功能强大且复杂
class类 在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类,虚拟机利用运行时类型信息选择相应的方法执行,然而,可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为Class,这个字很容易让人混淆,
Object类中的getClass()方法将会返回一个Class类型的实例。最常用的
Class方法是getName(不是静态的),这个方法将返回类的名字,如果被封装在包里,包名也作为类名的一部分,由于一些历史原因,对数组变量使用时可能会返回奇怪的名字class类的
forName方法,Class.forName(className)返回字符串className对应的class类对象T.class,如果T代表java类型的关键字(包括void),T.class将代表匹配的类对象
Class类实际上是一个泛型类。例如,Employee.class的类型是
Class<Employee>。没有说明这个问题的原因是:它将已经抽象的概念更加复杂化了。在大多数实际问题中,可以忽略类型参数,而使用原始的Class类。
利用反射类检查类结构
在
java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的域、方法和构造器。 这三个类都有一个叫做getName的方法, 用来返回项的名称。Held类有一个getType方法, 用来返回描述域所属类型的Class对象Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。这三个类还有一个叫做
getModifiers的方法, 它将返回一个整型数值,用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外, 还可以利java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。例如, 可以使用Modifier类中的isPublic、isPrivate或isFinal判断方法或构造器是否是public、private或final。 我们需要做的全部工作就是调用Modifier类的相应方法,并对返回的整型数值进行分析,另外,还可以利用Modifier.toString方法将修饰符打印出来。Class类中的
getFields、getMethods和getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields、getDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。使用反射在运行中分析对象(粗讲)
查看字段的具体内容:查看对象域的关键方法是
Field类中的get方法(对私有域不行,会得到一个错误),除非拥有访问权限,否则Java安全机制只允许査看任意对象有哪些域,而不允许读取它们的值,反射机制的默认行为受限于Java的访问控制然而, 如果一个Java程序没有受到安全管理器的控制,就可以覆盖访问控制。为了达到这个目的,需要调用
Field、Method或Constructor对象的setAccessible方法。例如,f.setAtcessible(true); /* now OK to call */ f.get(harry);通过反射实现通用的toString方法(但有时还是需要重新实现,能够不受控地访问类内部的日子已经屈指可数)

- 使用反射也可以编写泛型数组代码,并且可以调用任何方法和构造器
2.13 继承的设计技巧
- 将公共字段和方法设计在超类里
- 不要使用受保护(protected)的字段(虽然有时更方便,但是子类的集合是无限的,子类和同包下的所有类都将可以访问它,破坏了封装性)
- 使用继承实现
is-a关系 - 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期设想的行为
- 使用多态,而不要使用类型信息(只要看到类似的判断该对象类型进行不同操作的代码,都应该考虑使用多态)
- 不要滥用反射
