第三章 类,对象,方法
建议31:在接口中不要存在实现代码
接口是一种契约,框架协议,表明它的实现类都是同一种类型,或者具备相似特征的一个集合体。
建议32:静态变量一定要先声明后赋值
静态变量在类初始化时候首先被加载。
public static int i = 1;
static{
i = 100;
}
建议33:不要覆写静态方法
@Override针对非静态方法
一个实例对象有2个类型,表面类型(Apparent Type),实际类型(Actual Type)。
表面类型是声明时的类型
实际类型是对象产生时候的类型。
非静态方法是根据对象的实际类型执行的。
静态方法特殊,不依赖实例对象,通过类名访问,也可以通过对象访问。对象访问的话,JVM会通过对象的表面类型找到静态方法入口。
在子类中构建与父类相同的静态方法,这种行为叫“隐藏”。
通过实例对象访问静态方法或者属性不是好习惯。
建议34:构造函数尽量简化
构造函数初始化顺序:
子类实例化,首先初始化父类(初始化不是生产父类对象)初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类的构造函数。最后才生产一个实例对象。
构造函数,用于初始化变量,声明实例的上下文。
建议35:避免在构造函数中初始化其他类
不要在构造函数中声明初始化其他类,是良好习惯。
否则会造成StackOverflowError
建议36:使用构造代码块精炼程序
代码块:用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合。
Java中有4种类型代码块:
(1)普通代码块
方法后面用{},必须通过方法名调用。
(2)静态代码块
static{},用于静态变量初始化,对象创建前的环境初始化。
(3)同步代码块
synchronized{},表示同一时间只能一个线程进入该代码块。一种线程保护机制。
(4)构造代码块
类中没有任何前缀、后缀,用{}括起来。
编译器如果处理构造代码块?编译器会把构造代码块插入到每个构造函数的最前端。构造代码块会在每个构造函数内首先执行。
应用场景:在每个构造函数中运行,在构造函数中首先运行。
初始化实例变量
初始化实例环境
public class Client{
{
//构造代码块
System.out.println("构造代码块");
}
Client(){
System.out.println("无参构造函数");
}
Client(String str){
System.out.println("有参构造函数");
}
}
建议37:构造代码块会想你所想
统计一个类实例的数量
构造函数调用自身其他的构造函数(this关键字),则不插入构造代码块。
class Base{
//对象计数器
private static int numOfObjects = 0;
{
//构造代码块
numOfObject++;
}
public Base(){}
//有参调用无参构造
public Base(String str) {
this();
}
//有参不调用其它构造
public Base(int i){
}
public static int getNumOfObjects(){
return numOfObjects;
}
}
public class Client{
public static void main(String[] args){
new Base();
new Base("");
new Base(0);
System.out.println(Base.getNumOfObjects());
}
}
count = 3
建议38:使用静态内部类提高封装性
Java中嵌套类(Nested Class)分为两种:静态内部类(Static Nested Class) 和内部类(Inner Class)。
静态内部类2个优点:加强了类的封装性和提高了代码的可读性。形似内部,神似外部。编译后类文件名也包含外部类,外部类$内部类。他们之间是强关联关系。
静态内部类和普通类的区别:
(1)静态内部类不持有外部类的引用。普通内部类,可以直接访问外部类的属性和方法。静态内部类,只可以访问外部类的静态方法和静态属性,其他不行。
(2)静态内部类不依赖外部类
普通内部类和外部类之间是依赖关系,内部类不能脱离外部类实例,同生共死不能独立存在。静态外部类可以独立存在,即是外部类消亡,其还可以存在。
(3)普通内部内不能声明static变量和方法
普通内部内不能声明static变量和方法,常量(final static)还是可以的,静态内部类没有任何限制。
public class Person{
private String name;
private Home home;
Person(String name){
this.name = name;
}
public static class Home{
private String address;
private String tel;
Home(String address, String tel) {
this.address =addresss;
this.tel = tel;
}
}
}
建议39:使用匿名类的构造函数
List l1 = new ArrayList();
List l2 = new ArrayList(){};
List l3 = new ArrayList(){{}};
List l4 = new ArrayList(){{}{}{}{}{}{}};//也可以是多个
l2是匿名类的声明和赋值,定义了一个继承于ArrayList的匿名类。代码类似于:
class Sub extends ArrayList{
}
List l2 = new Sub();
l3是匿名类的声明和赋值。代码类似于:
class Sub extends ArrayList{
{
//初始化块,充当构造函数功能
}
}
List l3 = new Sub();
建议40:匿名类的构造函数很特殊
一般类(显式名字的类)所有构造函数默认都是调用父类的无参构造。
匿名类构造函数处理机制,匿名类没有名字,只能有构造代码块代替,在其初始化的时候直接调用父类同参数构造,然后再调用自己的构造函数。
enum Ops{ADD, SUB};
class Calculator{
private int i,j,result;
public Calculator(){};
public Calculator(int i, intj){
this.i= i;
this.j = j;
}
//设置运算符号
protected void setOperator(Ops ops){
result = ops.equals(Ops.ASS) ? i + j : i - j;
}
//获得运算结果
public int getResult(){
return this.result;
}
}
public static void main(String[] args){
Calculator cl = new Calculator(1,2){
setOperator(Ops.ADD);
}
System.out.println(cl.getResult());
}
//上面匿名函数相当于
class Add extends Calculator{
{
setOperator(Ops.ADD);
}
//覆写父类的构造方法
public Add(int i, int j){
super(i, j);
}
}
建议41:让多重继承成为现实
一个类可以多个接口,但不能继承多个类。有时候确实需要继承多个类,Java提供内部类曲折解决此问题。
interface Father{
public int strong();
}
interface Mother{
public int kind();
}
class FatherImpl implements Father{
public int strong(){
return 8;
}
}
class MotherImpl implements Mother{
public int kind(){
return 8;
}
}
class Son extends FatherImpl implements Mother{
@Override
public int strong(){
return super.strong() + 1;
}
@Override
public int kind(){
return new MotherSpecial.kind();
}
//内部类
private class MotherSpecial extends MotherImpl{
public int kind(){
return super.kind() - 1;
}
}
}
//女儿继承了母亲的kind,同时覆写了父类的strong,这里建立了一个匿名内部类来覆写父类的方法。
class Daughter extends MotherImpl implements Father{
@Override
public int strong(){
return new FatherImpl(){
@Override
public int strong(){
return super.strong() - 2;
}
}.strong();
}
}
建议42: 让工具类不可实例化
Java工具类的方法和属性都是静态的。由于不希望被初始化,于是设置构造函数为private权限,表示除了类本身外,谁都不能产生一个实例。工具类不要继承。
public class UtilsClass{
private UtilsClass(){
throw new Error("不要实例化我");
}
}
建议43:避免对象的浅拷贝
一个类实现了Cloneable接口表示具备了被拷贝能力,覆写了clone()方法完全具备拷贝能力。
浅拷贝,对象属性拷贝不彻底的问题。
public class Person implements Cloneable{
//姓名
private String name ;
//父亲
private Person father ;
Person(String name){
this.name = name ;
}
Person(String name, Person parent){
this.name = name ;
this.father = parent ;
}
public String getName() {
return name ;
}
public void setName(String name) {
this.name = name ;
}
public Person getFather() {
return father ;
}
public void setFather(Person father) {
this.father = father ;
}
@Override
public Person clone(){
Person p = null;
try {
p = (Person) super.clone();
p.setFather( new Person(p .getFather().getName()));
} catch (CloneNotSupportedException e ) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return p ;
}
}
public class Client {
public static void main(String[] args) {
//定义父亲
Person f = new Person("父亲" );
//定义大儿子
Person s1 = new Person("大儿子" , f );
//小儿子
Person s2 = s1 .clone();
s2.setName("小儿子");
//认干爹
s1.getFather().setName( "干爹");
System. out.println(s1 .getName() + " 父亲是: " + s1.getFather().getName());
System. out.println(s2 .getName() + " 父亲是: " + s2.getFather().getName());
}
}
建议44:推荐使用序列化实现对象拷贝
序列化实现对象拷贝比每一个类都写clone更好的办法
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class CloneUtils {
//拷贝一个对象
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj) {
//拷贝产生的对象
T cloneObj = null;
try {
//读取对象字节顺序
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
//分配内存空间,写入原始对象,生成新对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
//返回新对象并作类型转换
cloneObj = (T)ois.readObject();
ois.close();
}catch(ClassNotFoundException | IOException e) {
e.printStackTrace();
}
return cloneObj;
}
}
上面那个Person类implements Serializable加上SerialVersionUID就可以实现深拷贝
注意两点:
(1)对象内部的属性都是可以序列化的,如内部属性不可序列化,会抛出异常。
(2)注意方法和属性的特殊修饰符
final,static的问题,transient变量的问题。
更简便的方法是,用Apache commons工具包中的SerializationUtils类
建议45:覆写equals方法时候不要识别不出自己
在写一个JavaBean时,经常会覆写equals方法。根据业务规则判断两个对象是否相等。
equals方法的自反性原则:对于任何非空引用x, x.equals(x)应该返回true。
不要在p.getName()后面在加上.trim()。
例子见46
建议46:equals应该考虑null值情景
equals对称性原则:对于任何引用x和y的情景,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
//Object Equals
public class PersonEqual {
String name;
PersonEqual(String _name){
this.name = _name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean equals(Object obj){
if(obj != null && obj.getClass() == this.getClass()){
PersonEqual p = (PersonEqual)obj;
if(p.getName() == null || this.name == null)
return false;
else
return this.name.equalsIgnoreCase(p.getName());
}
return false;
}
}
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
PersonEqual pe1 = new PersonEqual("yao");
PersonEqual pe2 = new PersonEqual("yao ");
List<PersonEqual> list = new ArrayList<>();
list.add(pe1);
list.add(pe2);
System.out.println(list.contains(pe1));
System.out.println(list.contains(pe2));
}
}
//建议47:在equals中使用getClass进行类型判断
equals传递性原则:对于实例对象x,y,z。如果,x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。
//不是同一个类的2个值不能比较相等
//覆写equals时候,用getClass()代替instanceof判断
public class Emp extends PersonEqual {
private int id;
Emp(String _name, int _id) {
super(_name);
this.id=_id;
// TODO Auto-generated constructor stub
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public boolean equals(Object obj){
if(obj != null && obj.getClass() == this.getClass()) {
Emp emp = (Emp)obj;
return super.equals(obj) && emp.getId() == this.id;
}
return false;
}
}
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
Emp e1 = new Emp("yao", 100);
Emp e2 = new Emp("yao", 1001);
PersonEqual pe = new PersonEqual("yao");
System.out.println(pe.equals(e1));
System.out.println(pe.equals(e2));
}
}
建议48:覆写equals方法必须覆写hashCode方法
List中Person类的equals覆写了,不再判断两个类的地址是否相等,而是根据equals中的name判断两个对象是否相等。
HashMap的底层处理机制是数组的方式保存MAP条目(Map Entry),关键是数组下标的处理机制。根据传入元素hashCode方法的返回值决定其数组的下标。
如果,该数组位置上已经有了Map条目,并且与传入的键值相等则不处理。
如果,不想等则覆盖;如果数组位置没有条目,则插入,并加入到Map条目的链表中。
检查键是否存在也是根据哈希码定位的,然后遍历查找键值。
hashCode方法的返回值是什么?是一个对象的哈希码。由Object类的本地方法生成,确保每一个对象有一个哈希码。
重写Person类的hashCode方法。
//Person HashCode
public class PersonHashCode {
String name;
PersonEqual(String _name){
this.name = _name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean equals(Object obj){
if(obj != null && obj.getClass() == this.getClass()){
PersonEqual p = (PersonEqual)obj;
if(p.getName() == null || this.name == null)
return false;
else
return this.name.equalsIgnoreCase(p.getName());
}
return false;
}
@Override
public int hashCode(){
return new HashCodeBuilder().append(this.getName).toHashCode();
}
}
public static void main(String[] args){
//Person类作为Map的Key
Map<Person, Object> map = new HashMap<>{
{
put(new Person("张三"),new Object());
}
};
//Person类的实例作为List的元素
List<Person> list = new ArrayList<>{
{
add(new Person("张三"));
}
};
boolean b1 = list.contains(new Persion("张三"));
boolean b2 = map.containsKey(new Person("张三"));
}
建议49:推荐覆写toString方法
Java本身提供的toString方法不友好。
ToStringBuilder //代替Javabean中的toString()方法
建议50:使用package-info类为包服务
特殊的类:package-info
专门为包服务。描述和记录包的信息。
不能随便被创建,服务的对象特殊,此类不能有实现代码。
作用:
(1)声明友好类和包内访问常量
//这里是类包,声明一个包使用的公共类
class PkgClass{
public void test(){}
}
//包常量,只允许包内访问
class PkgConst{
static final String PACKAGE_CONST = "ABC";
}
(2)为包上提供注解便利
写一个注解标注,放到package-info文件中。
(3)提供包的整体注释说明
建议51:不要主动进行垃圾回收