继承
作用
- 提高了代码的复用性。
- 让类与类之间产生了关系。有了这个关系,才有了多态的特性。
代码示范
父类代码
public class Parent {
public void say() {
System.out.println("父类的say方法");
}
}
子类代码,继承父类,也就拥有了say方法
public class Son extends Parent {
}
测试代码
public class Main {
public static void main(String[] args) {
Son son=new Son();
son.say();
/**
* 输出结果
* 父类的say方法
*/
}
}
注意事项
- 不要为了某些功能而继承,继承之间应该是is a关系。
- Java只支持单继承
- Java支持多层级继承,例如3继承2,2继承1,那么3就拥有1和2的方法。
this和super关键字
简介
this代表调用该方法的引用,而super则代表父类对象的引用。当我们在类内部要使用自己的方法时可以使用this,要调用父类方法时,可以使用super。
this和super使用实例
父类的方法
public class Parent {
public void say() {
System.out.println("父类的say方法");
}
}
子类方法
public class Son extends Parent {
public void say2(){
super.say();
System.out.println("父类say完子类say");
}
}
测试输出结果
public class Main {
public static void main(String[] args) {
Son son=new Son();
son.say2();
/**
* 输出结果
* 父类的say方法
* 父类say完子类say
*/
}
}
继承工作原理解析
代码示例
可以看到父类的代码如下所示,设置一个num为3
public class Parent {
private int num=3;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
子类继承父类,num设置为4
public class Son extends Parent {
private int num = 4;
public void show() {
System.out.println(this.num);
}
}
输出结果,可以看到输出的是子类的值,当然如果show中用的是super.getNum(),输出结果就为3,那么jvm是如何工作的呢?
public class Main {
public static void main(String[] args) {
Son son=new Son();
son.show();
/**
* 输出结果
* 4
*/
}
}
图解子父类初始化过程
- 方法区的非静态区加载父类和子类的信息
- 实例化son类时,堆区开辟一个空间
- 若son无num,super和this指向的都是父类的num,反之只有thi指向自己的num
重写
概述
子类编写一个方法参数和名字都和父类一样时,就会执行子类重写的代码,并且我们需要使用@Override
关键字注明这个方法是重写的方法。
代码示例
父类的代码示例
public class Parent {
public List getList() {
return Collections.emptyList();
}
}
子类的代码示例
public class Son extends Parent {
@Override
public List getList() {
return super.getList();
}
}
注意事项
-
子类重写父类方法是必须遵循两小一大原则。
1. 访问权限大于父类 2. 抛出异常小于父类 3. 返回值小于父类
代码如下所示,可以看到子类抛出的异常以及返回值都小于父类
父类
public class Parent {
public List getList() throws Exception {
return Collections.emptyList();
}
}
子类
public class Son extends Parent {
@Override
public ArrayList getList()throws IllegalAccessException {
return new ArrayList();
}
}
- 重写时希望保留父类的逻辑可以在代码首行用代码覆盖一下。
public class Parent {
public void say(){
System.out.println("父类的say方法");
}
}
如下所示子类基于父类的方法实现了自己的特定逻辑
public class Son extends Parent {
@Override
public void say() {
super.say();
System.out.println("子类的say方法");
}
}
子父类中的构造函数
- 子类对象进行初始化时,会隐式的调用父类的无参构造方法。
- 如果你要显示调用父类构造方法一定要在第一行声明,否则你做的所有操作都有可能会被父类构造方法覆盖。
代码示例如下所示
父类的代码
public class Parent {
public Parent() {
System.out.println("父类的构造方法");
}
public void say(){
System.out.println("父类的say方法");
}
}
子类的代码
public class Son extends Parent {
@Override
public void say() {
super.say();
System.out.println("子类的say方法");
}
}
输出结果如下所示,可以看到父类构造函数的方法的输出结果输出了
public class Main {
public static void main(String[] args) {
Son son=new Son();
son.say();
/**
* 输出结果
* 父类的构造方法
* 父类的say方法
* 子类的say方法
*/
}
}
final关键字
关键字简介
- final关键字可以修饰类、方法、变量
- 修饰类,则这个类不可被继承
- 修饰方法则这个方法不可便重写
- 修饰变量,若为引用类型则该引用指向地址不可被修改,但是引用可以被修改。若基础类型则值不可修改。而且这个关键字可以修饰成员变量和局部变量
代码示例
public final class Parent {
private final int a = 3;
public Parent() {
final int c=2;
System.out.println("父类的构造方法");
}
public final void say() {
System.out.println("父类的say方法");
}
}
抽象类
使用场景
当多个类中出现相同功能,但是功能主体不同,这是可以进行向上抽取。这时,只抽取功能定义,而不抽取功能主体。
抽象类特点
- 抽象方法一定在抽象类中。
- 抽象方法和抽象类都必须被
abstract
关键字修饰。 - 抽象类不可以用new创建对象。因为调用抽象方法没意义。
- 抽象类中的抽象方法要被使用,必须由子类继承并实现所有的抽象方法后,才能建立子类对象调用。如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。
抽象类和一般类没有太大的不同。
该如何描述事物,就如何描述事物,只不过,该事物出现了一些暂时不知道如何实现或者实现方式不一定。
这些不确定的部分,也是该事物的功能,需要明确出现。但是无法定义主体。这时,通过抽象方法来表示最为合适。
抽象类常见问题
abstract 关键字,和哪些关键字不能共存?
- final:被final修饰的类不能有子类。而被abstract修饰的类一定是一个父类。
- private: 抽象类中的私有的抽象方法,不被子类所知,就无法被复写。而抽象方法出现的就是需要被复写。
- static:如果static可以修饰抽象方法,那么连对象都省了,直接类名调用就可以了。可是抽象方法运行没意义。
抽象类常见使用场景——模板方法模式
抽象类常常作为模板方法的实施方案,以下便是笔者之前写过关于模板方法的文章,感兴趣的可以看看
设计模式-模板方法
接口
格式特点:
- 接口中常见定义:常量,抽象方法。
- 接口中的成员都有固定修饰符。
常量:public static final
方法:public abstract
记住:接口中的成员都是public的。
接口和抽象类有什么共同点和区别?
共同点
- 接口和抽象类都可以包含抽象方法和默认方法
- 都不能被实例化
不同点
- 接口的变量必须是public static final修饰,抽象类可以默认不赋初值
- 类可以继承多接口,抽象类只能继承一个
- 接口常用于定义行为,继承该接口的类就会拥有某些行为,而抽象类则是抽取共性提供抽象方法给子类实现
多态概述
什么是多态
多态即事物可以有多种存在形态,如:猫可以是猫类也可以是动物类
代码示例
Cat cat=new Cat();
Animal animal=new Cat();
多态的体现
父类引用指向子类对象,通俗来说就是父类引用可以指向自己的对象
public void static main(String[] args){
function(new Cat());//会输出cat的eat方法而不是Animal的
function(new Dog());//会输出dog的eat方法而不是Animal的
}
public static void function(Animal a)//Animal a = new Cat();
{
a.eat();
}
多态的好处
提高了程序的扩展性,使得同一个函数得以复用,方便后续开发的扩展。
使用多态的前提
- 必须是类与类之间有继承(extends)或者实现关系(写一个接口让另一个类implement)
- 存在对父类的覆盖
多态的弊端
只能使用父类引用访问父类成员,即cat继承Animal类并且覆盖Animal的eat或者相关方法。
多态的转型
Animal a=new Cat();//父类指向子类 向下转型
if(a instanceof Cat )//判断是否该animal类是否可以转为cat
Cat c=(Cat)a;//强转为cat 向下转型
多态的常见面试题
成员函数在多态调用输出结果是什么?
答:编译看左边,运行看右边
在多态中成员函数的特点:
在编译时期:参阅引用型变量所属的类中是否有调用的方法。如果有,编译通过,如果没有编译失败。
在运行时期:参阅对象所属的类中是否有调用的方法。
父类
public class Parent {
public void func() {
System.out.println("Parent");
}
}
子类
public class Son extends Parent {
@Override
public void func() {
System.out.println("Son");
}
}
输出结果可以看到,编译看左边,输出结果完全看右边
public class Main {
public static void main(String[] args) {
Parent p = new Son();
p.func();
/**
* 输出结果
* Son
*/
}
}
成员变量编译和结果取决于什么?
成员变量编译和结果全都取决于引用类型,即左边声明的类名,代码如下所示
父类
public class Parent {
public int num=1;
public void func() {
System.out.println("Parent");
}
}
子类
public class Son extends Parent {
public int num=2;
@Override
public void func() {
System.out.println("Son");
}
}
可以看到输出解决是父类的数据
public class Main {
public static void main(String[] args) {
Parent p = new Son();
System.out.println(p.num);
/**
* 输出结果
* 1
*/
}
}
静态函数编译运行结果取决于什么?
全看左边的引用类型,引用类型是什么就输出什么
class Fu
{
static void method4()
{
System.out.println("fu method_4");
}
}
class Zi extends Fu
{
static void method4()
{
System.out.println("zi method_4");
}
}
class Test
{
public static void main(String[] args)
{
Fu f = new Zi();
f.method4();//看该对象引用类型 所以输出fu method_4
Zi z = new Zi();
z.method4();//看该对象引用类型 所以输出zi method_4
}
}
图解代码运行原理
如下图,静态区加载的是类信息,而不是this和spuer关键字,而且静态方法也不可以被重写所以,所以运行结果就根据左边的引用类型决定
object类
概述
Object:是所有对象的直接后者间接父类,传说中的上帝。
该类中定义的肯定是所有对象都具备的功能。
equals和toString
equals方法
比较的是对象的地址空间是否相同
toString方法
输出的是对象的类型+“@”+内存地址的十六进制
Object类中已经提供了对对象是否相同的比较方法。
如果自定义类中也有比较相同的功能,没有必要重新定义。
只要沿袭父类中的功能,建立自己特有比较内容即可。这就是覆盖。
代码
class Demo //extends Object
{
private int num;
Demo(int num)
{
this.num = num;
}
//覆盖原有的equals
public boolean equals(Object obj)//Object obj = new Demo();
{
if(!(obj instanceof Demo))
return false;
Demo d = (Demo)obj;
return this.num == d.num;
}
//覆盖原有的toString
public String toString()
{
return "demo:"+num;
}
}
class Person
{
}
class ObjectDemo
{
public static void main(String[] args)
{
Demo d1 = new Demo(4);
System.out.println(d1);//输出语句打印对象时,会自动调用对象的toString方法。打印对象的字符串表现形式。
Demo d2 = new Demo(7);
System.out.println(d2.toString());//打印的是d2.getName()+"@@"+Integer.toHexString(d2.hashCode()
}
}
创建和销毁对象注意事项
用静态工程替代构造器
遇到多构造器参数建议使用建造者模式
优点:
1. 保证类的不可变
2. 实现的可变参数
3. 增加可读性
/**
* 遇到多构造器参数建议使用建造者模式
* 优点:
* 1. 保证类的不可变
* 2. 实现的可变参数
* 3. 增加可读性
*/
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
private int servingSize;
private int servings;
// 可选参数,赋值上默认值
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
@Override
public String toString() {
return "NutritionFacts{" +
"servingSize=" + servingSize +
", servings=" + servings +
", calories=" + calories +
", fat=" + fat +
", sodium=" + sodium +
", carbohydrate=" + carbohydrate +
'}';
}
public NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}
}
对象创建示例
public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts.Builder(240, 8).calories(300).sodium(50).build();
System.out.println(nutritionFacts.toString());//NutritionFacts{servingSize=240, servings=8, calories=300, fat=0, sodium=50, carbohydrate=0}
}
避免创建不必要的对象
频繁创建对象会增加GC回收压力以及系统开销,所以我们应该避免创建不必要的对象
下面这段代码,从源码我们就可以看到matches方法会创建一个Pattern,所以我们可以对正则匹配的方法进行响应重构
// Performance can be greatly improved! (Page 22)
static boolean isRomanNumeralSlow(String s) {
/**
* 底层实际会创建一个Pattern 频繁调用会创建多个Pattern对象
* Pattern p = Pattern.compile(regex);
* Matcher m = p.matcher(input);
* return m.matches();
*/
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
所以我们建议基于matches底层调用方式自己手写一个match方法避免没必要的对象创建
/**
* 手动创建pattern避免频繁创建对象
*/
private static final Pattern pattern = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeralFast(String s) {
return pattern.matcher(s).matches();
}
消除过期的对象引用
如下所示,Stack 的elements成员变量是一个数组,当我们调用pop逻辑上是将栈顶元素弹出,实际上jvm是无法感知这种逻辑弹出的,所以我们需要手动消除这个对象引用,否则会出现OOM异常
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
//引用指向数组,我们所谓的size以及活动非活动元素对于虚拟机来说都是无感的
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object element = elements[--size];
//消除过期引用
elements[--size] = null;
return element;
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
while (true)
System.err.println(stack.pop());
}
}
try-with-resource优先于try-finally
使用try-with-resource相较于后者更加简洁、清晰、产生的异常信息也更有价值
public class Copy {
private static final int BUFFER_SIZE = 8 * 1024;
// try-finally is ugly when used with more than one resource! (Page 34)
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
static void copyWithTryResource(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
public static void main(String[] args) throws Exception{
copy("D:\\source.txt","dst.txt");
copyWithTryResource("D:\\source.txt","dst2.txt");
}
}
参考文献
Java基础常见面试题总结(中)
Effective Java中文版(第3版)
Java程序性能优化