1 类成员
1.1 成员变量(类变量、实例变量)和局部变量
- 语法形式:静态变量属于类;实例变量属于对象;局部变量是在代码块或方法中定义的变量
- 存储方式:静态变量存储在方法区;实例变量随着对象存储在堆;局部变量存储在栈。
- 生存时间:静态变量从类的准备阶段起开始存在,直到JVM完全销毁这个类。实例变量随着对象的创建而存在,随着对象的消亡而消亡。局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值:静态变量和实例变量不用赋初始值就可以使用;局部变量必须赋初始值才可以使用。
1.2 方法的重写和重载
- 方法重写:子类对父类的非私有方法的实现过程进行重新定义
- 参数列表不能改变
- 返回值不能改变
- 能抛出新的或者更广的异常
- 访问权限不能更低
- 方法重载:发生在同一个类里面具有相同名字的方法
- 参数列表必须改变
- 返回类型可修改也可以不修改
- 抛出的异常类型可修改也可以不修改
- 访问权限可修改也可以不修改
1.3 构造方法
构造方法是一种特殊的方法,作用是完成对象的初始化工作。如果一个类没有声明构造方法,编译期会默认的添加一个无参构造器。构造方法不能被继承,不能被重写,但是可以重载。在一个类中,可以有多个构造方法(方法参数不同),即重载,来实现对象属性不同的初始化。
1.4 代码块
Java使用构造器先完成整个Java对象的状态初始化,然后将Java对象返回给程序,从而让该Java对象的信息更加完整。而初始化块的作用与构造器的作用类似,也可以对Java对象进行初始化操作。
1.5 内部类
内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。假设需要创建Cow类,Cow类需要组合一个CowLeg对象,CowLeg类只有在Cow类里才有效,离开了Cow类之后没有任何意义。在这种情况下,就可把CowLeg定义成Cow的内部类,不允许其他类访问CowLeg。根据内部类的类型不同,可以细分为:
- 非静态内部类
- 静态内部类
- 局部内部类
- 匿名内部类
2 面向对象编程特性
2.1 面向对象和面向过程
- 面向过程:把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题
- 面向对象:会先抽象出对象,然后通过调用对象方法的方式去解决问题
2.2 封装继承多态
- 封装:封装是指把对象的属性隐藏在对象内部,不允许外部对象直接访问对象属性。但是可以提供一些可以被外界访问的方法来操作属性。优点:提高了数据的安全性。隐藏了实现,只需调用方法即可。
- 继承:不同类型的对象,有一定的共同点,可以使用继承的方式将共同点抽取出来。比如学生类和教师类都有人类的基本行为,可以抽象出一个Person的公共父类。优点:提高代码的复用性和可维护性。
- 多态:一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。成员变量,静态方法编译和运行都看左边。实例方法编译看左边,运行看右边。优点:利于程序的扩展,比如许多框架就是基于多态原理的实现。Java提供了两种用于多态的机制,分别是编译时多态与运行时多态:
- 编译时多态:基于方法重载实现的,在编译期间就可以确定调用哪个方法。
- 运行时多态:基于重写父类的方法来实现的,然后使用父类或父接口指向子类实例对象,在运行期根据具体的实例对象的方法来确定调用哪个方法。
2.3 访问权限修饰符
修饰符 | 同类 | 同包 | (不同包)子类 | 其他包 |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
default | Y | Y | N | N |
private | Y | N | N | N |
2.4 非访问权限修饰符
- static:使用static修饰的成员为静态成员,是属于某个类的,并且保存在方法区中
- final:用来修饰类、方法和变量
- abstract:用来创建抽象类和抽象方法
- synchronized:用于实现同步方法和同步代码块
- volatile:修饰的成员变量在每次被访问时,都强制从共享内存重新读取该成员变量的值
- transient:修饰的实例变量在进行序列化时会跳过
访问权限修饰符 | 非访问权限修饰符 | |
---|---|---|
类 | public、默认 | abstract、final |
内部(地位同成员变量) | public、protected、默认、private | static |
方法 | public、protected、默认、private | final、static、synchronized、native、abstract |
成员变量 | public、protected、默认、private | final、static、transient、volatile |
局部变量 | final |
需要注意的是:
- protected和private不能修饰外部类,因为类只有其他包可见和其他包不可见两种情况
- final和abstract不能同时修饰类,因为该类要么能被继承要么不能被继承
- static不能修饰类和接口,因为类加载后才会加载静态成员变量
- final不能修饰接口和抽象类
2.5 final关键字
- final类变量和成员变量:Java规定final修饰的类变量和成员变量必须显式地指定初始值,因为如果不显式指定初始值的话,系统会默认给它一个默认值。类变量:定义时赋值或者在静态代码块中赋值。成员变量:定义时赋值或者或者在代码块和构造器中赋值。
- final局部变量:系统不会对局部变量进行初始化,因此局部变量也必须显式初始化,可以不在定义是赋初始化值,但在使用前必须赋值。
- final方法:final修饰的方法不可被重写,比如,Object类的getClass(),因为Java不希望这个方法被重写。
- final类:final修饰的类不可被继承。比如Math类就是一个final类,它不可以有子类。
2.6 final基本类型和引用类型
- 当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。
- 对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象的属性可以发生改变。
2.7 抽象类和接口的对比
相同点:接口和抽象类都不能被实例化。接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。都可以有默认方法。
不同点:
- 接口体现的是一种规范,对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务。抽象类体现的是一种模板,主要用于代码复用,强调的是所属关系。
- 接口只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现。抽象类则可以包含普通方法。
- 接口里只能定义静态常量。抽象类里则既可以定义静态常量,也可以定义普通成员变量。
- 接口里不包含构造器。抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 接口里不能包含初始化块。但抽象类则完全可以包含初始化块。
2.8 引用拷贝、浅拷贝、深拷贝
- 引用拷贝:引用拷贝就是复制多一个不同的引用指向同一个对象。
- 浅拷贝:浅拷贝会在堆上创建一个新的对象。如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
浅拷贝代码:实现Cloneable接口,并重写clone()方法。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address implements Cloneable {
private String name;
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Cloneable {
private Address address;
@Override
public Person clone() {
try {
return (Person) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class Test {
public static void main(String[] args) {
Person person1 = new Person(new Address("武汉"));
Person person2 = person1.clone();
System.out.println(person1 == person2); // false
System.out.println(person1.getAddress() == person2.getAddress()); // true,属性未被拷贝
}
}
深拷贝代码:需要对clone()方法进行重新定义,不能简单的调用父类Object的clone()。下述提供常见的四种方法:
public class Address implements Cloneable, Serializable {
private String name;
@Override
public Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
}
public class Person implements Cloneable, Serializable {
private String name;
private int age;
private Address address;
/**
* 浅拷贝
*
*/
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
/**
* 成员变量发生变动需要修改方法,不满足开闭原则
* 不具有可复用性
*/
public Person deepCopy1() {
Person copyPerson = new Person().setName(this.getName())
.setAge(this.getAge());
Address address = this.getAddress();
if (address != null) {
Address copyAddress = new Address().setName(address.getName());
copyPerson.setAddress(copyAddress);
}
return copyPerson;
}
/**
* 成员变量发生变动需要修改方法,不满足开闭原则
* 不具有可复用性
*/
public Person deepCopy2() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.setAddress(person.getAddress().clone());
return person;
}
// 推荐
public Person deepCopy3() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Person) ois.readObject();
}
public Person deepCopy4() {
String jsonString = JSON.toJSONString(this);
return JSONObject.parseObject(jsonString, new TypeReference<Person>() {
});
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Address address = new Address()
.setName("深圳");
Person person = new Person().setName("小明")
.setAge(20)
.setAddress(address);
Person person1 = person.deepCopy1();
Person person2 = person.deepCopy2();
Person person3 = person.deepCopy3();
Person person4 = person.deepCopy4();
System.out.println(person.getAddress() == person1.getAddress());
System.out.println(person.getAddress() == person2.getAddress());
System.out.println(person.getAddress() == person3.getAddress());
System.out.println(person.getAddress() == person4.getAddress());
}
}
3 Object类
3.1 Object 11个方法
对象相关: getClass()、hashCode()、equals(Object obj)、clone()、toString()
多线程相关: notify()、notifyAll()、wait()、wait(long timeout)、wait(long timeout, int nanos)
垃圾回收相关: finalize()
3.2 ==和equals()
- ==: 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址
- equals(): equals()是方法,不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。如果对象没有重写equals(),那么它和==作用等价。一般我们都会重写equals()来比较两个对象中的属性是否相等,若它们的属性相等,则返回true。比如,String中的equals()是被重写过的,因为从父类Object类继承得到的equals()是比较的对象的内存地址,而String的equals()比较的是对象的值。
3.3 hashcode()和equals()
hashcode()的作用是获取哈希码,当我们在HashMap或者HashSet中添加对象或者查找对象时,是根据这个这个哈希码去计算位置的。之所以还需要equals(),是因为可能会出现哈希碰撞的情况,所以需要equals()去找出目标对象。也就是说:
- 如果两个对象的哈希码相等,那这两个对象不一定相等(因此可能存在哈希冲突)
- 如果两个对象的哈希码相等并且equals()方法也返回true,我们才认为这两个对象相等
- 如果两个对象的哈希码不相等,这两个对象就不相等
重写equals()时必须重写hashCode():因为两个相等的对象的哈希码必须是相等,也就是说如果equals方法判断两个对象是相等的,那这两个对象的hashCode值也要相等。如果重写equals()时没有重写hashCode()的话就可能会导致equals()判断是相等的两个对象,哈希码却不相等。
4 String类
4.1 String、StringBuffer、StringBuilder
- 可变性:String是不可变的,StringBuilder与StringBuffer是可变的。
- 线程安全:String是不可变的,可以理解为常量,所以是线程安全。StringBuffer对方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
- 性能:操作少量的数据直接使用String,单线程环境下使用StringBuilder能够获得最好的性能,多线程环境下使用StringBuffer能够保证线程安全,但是效率稍差。
4.2 String为什么是不可变的
首先,实际保存字符串内容的value[]数组被final修饰且为private的,并且String类没有提供修改这个字符串的方法。
其次,String类被final修饰导致其不能被继承,进而避免了子类破坏String的不可变。
4.3 StringBuilder性能分析
字符串拼接用“+”还是StringBuilder:字符串对象通过“+”的字符串拼接方式,实际上是通过StringBuilder调用append()方法实现的,拼接完成之后调用toString()得到一个String对象。不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:每次循环都会创建一个新的StringBuilder,不能复用StringBuilder,会导致性能下降。
因此,字符串循环拼接效率:StringBuilder > StringBuffer > String
5 代码测试题
5.1 构造方法和代码块执行顺序
public class Father {
static {
System.out.println("Father类的静态代码块");
}
{
System.out.println("Father类的代码块");
}
public Father() {
System.out.println("Father类的无参构造器");
}
}
public class Son extends Father{
static {
System.out.println("Son类的静态代码块");
}
{
System.out.println("Son类的代码块");
}
public Son() {
// 默认添加super();
System.out.println("Son类的无参构造器");
}
public Son(int age) {
// 默认添加super();
System.out.println("Son类的有参构造器" + age);
}
}
public class Test {
public static void main(String[] args) {
System.out.println("---第一次调用:new Son();---");
new Son();
System.out.println("---第二次调用:new Son();---");
new Son();
System.out.println("---第三次调用:new Son();---");
new Son(6);
}
}
输出结果为:
---第一次调用:new Son();---
Father类的静态代码块
Son类的静态代码块
Father类的代码块
Father类的无参构造器
Son类的代码块
Son类的无参构造器
---第二次调用:new Son();---
Father类的代码块
Father类的无参构造器
Son类的代码块
Son类的无参构造器
---第三次调用:new Son();---
Father类的代码块
Father类的无参构造器
Son类的代码块
Son类的有参构造器6
解释:
第一次执行: JVM首先会进行类加载过程。在这个过程中,会先执行父类的静态代码块,再执行自己的静态代码块。编译之后的字节码文件,普通代码块的内容会放在每一个构造方法之内,并且在原来的构造方法内容之前。 所以会先执行父类的普通代码块,再执行父类的构造函数,最后再到自己的普通代码块和构造函数。
第二次执行: 因为类已经被加载过了,所以就不会执行静态代码块的内容了。 因此,会先执行父类的普通代码块,再执行父类的构造函数,最后再到自己的普通代码块和构造函数。
第三次执行: 如果是调用子类的有参构造器的话,那么在构造器的代码内部前面仍会默认加上父类的无参构造器。因此就是先执行父类的普通代码块,再执行父类的无参构造函数,然后再执行自己的普通代码块和构造函数。