Java语言02之面向对象编程

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 访问权限修饰符

修饰符同类同包(不同包)子类其他包
publicYYYY
protectedYYYN
defaultYYNN
privateYNNN

2.4 非访问权限修饰符

  • static:使用static修饰的成员为静态成员,是属于某个类的,并且保存在方法区中
  • final:用来修饰类、方法和变量
  • abstract:用来创建抽象类和抽象方法
  • synchronized:用于实现同步方法和同步代码块
  • volatile:修饰的成员变量在每次被访问时,都强制从共享内存重新读取该成员变量的值
  • transient:修饰的实例变量在进行序列化时会跳过
访问权限修饰符非访问权限修饰符
public、默认abstract、final
内部(地位同成员变量)public、protected、默认、privatestatic
方法public、protected、默认、privatefinal、static、synchronized、native、abstract
成员变量public、protected、默认、privatefinal、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首先会进行类加载过程。在这个过程中,会先执行父类的静态代码块,再执行自己的静态代码块。编译之后的字节码文件,普通代码块的内容会放在每一个构造方法之内,并且在原来的构造方法内容之前。 所以会先执行父类的普通代码块,再执行父类的构造函数,最后再到自己的普通代码块和构造函数。
第二次执行: 因为类已经被加载过了,所以就不会执行静态代码块的内容了。 因此,会先执行父类的普通代码块,再执行父类的构造函数,最后再到自己的普通代码块和构造函数。
第三次执行: 如果是调用子类的有参构造器的话,那么在构造器的代码内部前面仍会默认加上父类的无参构造器。因此就是先执行父类的普通代码块,再执行父类的无参构造函数,然后再执行自己的普通代码块和构造函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值