06、面向对象—三大特性


前言

      去年四月份大一下半学期正式开始学习Java,一路从java基础、数据库、jdbc、javaweb、ssm以及Springboot,其中也学习了一段时间数据结构。

      在javaweb期间做了图书商城项目、ssm阶段做了权限管理项目,springboot学了之后手痒去b站看视频做了个个人博客项目(已部署到服务器,正在备案中)。期间也不断进行做笔记,总结,但是越学到后面越感觉有点虚,觉得自己基础还有欠缺。

      之后一段时间我会重新回顾java基础、学习一些设计模式,学习多线程并发之类,以及接触一些jvm的相关知识,越学到后面越会感觉到基础的重要性,之后也会以博客形式输出学习的内容。

      现在整理的java知识基础点是在之前学习尚硅谷java课程的笔记基础之上加工汇总,部分图片会引用尚硅谷或网络上搜集或自己画,在重新回顾的过程中也在不断进行查漏补缺,尽可能将之前困惑的点都解决,让自己更上一层楼吧。

      博客目录索引博客目录索引(持续更新)



一、特征一:封装

封装介绍

为什么要封装?封装的含义与作用?

  • 举个例子,就像我们开车,车也分手动挡,自动挡,手动挡的话我们是不是起步需要挂一档,后面随着速度换不同的挡数;自动挡只要你踩踩油门就行,手动挡与自动挡都进行了封装,只不过自动挡封装的更好。一般来说不需要你去了解内部构造,也不想让你去修改内部构造。
  • 隐藏对象内部的复杂性,只对外公开简单的接口,便于外界调用。从而提高系统的可扩展性、可维护性。简单来说,把该隐藏的隐藏,该暴露的暴露,这就是封装性的设计思想。

程序设计高内聚,低耦合

  • 高内聚:类的内部操作细节自己完成,不允许外部干涉。
  • 低耦合:仅对外暴露少量的方法用于使用。

封装性的体现

  1. 例如对属性进行私有化(private),提供公共的方法来获取与设置属性,不随意让人直接获取并设置值。
  2. 想要获取并设置值只能通过特定的set/get方法来进行。
  3. 设计模式包含单例模式(获取实例不能通过new,而是通过一个公共方法获得),特定场景进行使用。

问题:也看到过某些setter没有做任何事情,仅仅只是将值设给变量而已,这样不是只会增加执行的负担吗?

答:这对getter也是一样的,好处是你事后可以改变想法却不会需要改变其他部分的程序。假设说所有人都使用你的类以及共有变量,万一有一天你发现这个变量需要检查,那不是所有人都要跟着改成调用setter嘛,这就是封装的优点之处,直接进行存取变量的效率的好处比不上其封装的好处。考虑到之后改变心意,将程序改的更安全、更快、更好。



权限修饰符

封装性的体现,需要权限修饰符来配合使用

权限修饰符privatepublicdefault(缺省)、protected

  • 缺省:没有权限修饰符的属性默认为缺省

修饰类与类的内部结构:属性、方法、构造器、内部类

  • 注意:四个权限修饰符对于这几个结构都能够使用。

  • 修饰的修饰符:publicdefault(缺省)

    • public class Main{
              public static void main(String[] args){\
          }
      }
      //默认是default(缺省)
      class Person{
      }
      

四种权限访问修饰符,如图



二、特征二:继承

1、继承介绍

继承性(inheritance)

继承好处介绍:减少代码冗余、提高复用性;便于功能扩展;多态的提前。

格式class A extends B{}

  • A:又叫子类、派生类、subclass。
  • B:又叫父类、超类、基类、superclass。
  • extends:表示延伸、扩展含义。

继承说明

  1. 子类A继承父类B之后,子类A能够获取父类B声明的所有属性与方法,并且能够声明自己持有的属性与方法,实现功能的扩展。
  2. 父类B中声明为private的私有属性及方法,子类A继承B仍然认为获取了父类中私有的结构,因为封装性的原因,在子类A中无法直接调用父类的结构而已。

继承规则规定

  1. 一个类可以被多个子类继承。
  2. Java具有单继承性,不允许多继承,一个类只能有一个父类。
  3. 子类直接继承的父类:直接父类。间接继承的父类:间接父类。
  4. 子类继承父类之后,不仅仅获取直接父类的属性与方法,还获取了所有间接父类的属性及方法。


2、方法的重写

重写(override/overwrite)

方法重写:在子类中根据对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。

规则

  • 子类对继承父类被重写的方法有相同方法名称、参数列表
  • 子类重写的方法返回值类型不能大于父类被重写方法的返回值类型。
    • 若父类被重写方法返回值为void,子类重写方法返回值只能也是void
    • .....返回值为A类型,....返回值可以是A类或A类的子类
    • ...返回值为基本类型,....返回值类型必须是相同的
  • 子类重写方法使用的访问权限不小于父类被重写方法的访问权限。
  • 子类不能重写父类中声明为private权限方法
  • 子类方法抛出异常不能大于父类被重写方法的异常。

注意:子父类中同名同参数的方法要在声明为非static的情况下才能重写。父类的方法若是static,子类与父类的同名同参数的方法必须也是static,但这并不是重写(static属于类,子类无法覆盖父类static方法,是独有的)。



3、super关键字

super:在子类中进行使用,具体有三个用处

情况一:在子父类中若有相同名字的属性,在内存中实际上是有两个不同的内存地址(方法可覆盖,属性不可覆盖),这个时候就需要通过使用this.子类属性super.父类属性来进行区分属性。

情况二:使用super.属性,若是直接父类没有,会自动往间接父类上去找。

情况三:super调用构造器:

  • 在子类中使用super(参数列表)来调用父类的构造函数
  • super(参数列表)必须声明在子类构造器的首行
  • 构造器中super(参数列表)this(参数列表)不能够同时出现
  • 子类如果继承父类的话,在子类的构造器中的首行若是你自己不定义super(参数列表)this(参数列表),会默认使用父类中空的构造器,也就是super(),若是此时父类恰好没有空参构造器就会报编译错误。


4、对象实例化过程

  1. 结果上来看(继承性):子类继承父类之后,会自动获取父类的属性或方法,在创建子类对象的过程中,堆空间不仅创建子类的内存空间,也会加载其直接父类、间接父类也就是所有父类声明的属性。
    • 构建一个对象时,jvm会在堆中给对象分配空间,这些空间用来存储当前对象实例属性以及其父类的实例属性。
  2. 过程来看:在通过子类构造器创建对象时,一定会直接或间接的调用父类的构造器,进而调用父类的父类构造器,一直到java.lang.Object类中的空参构造器为止。正因为加载过所有父类的结构,才可以看到父类中结构,子类对象才会考虑进行调用。
    • 不断的调用父类构造器也是进行栈的一个过程,栈顶的构造器执行完后才不断往下进行执行。

针对于创建的对象:虽然创建子类对象调用了父类的构造器,但是自始至终只创建过一个对象(实例化),也就是new的子类对象。(在对应堆中的内存空间中会加载父类的结构)

对于Snowboard只有Object父类时创建实例,堆中如下图:

image-20210128111112267



三、特征三:多态性

介绍多态

多态介绍,编译、运行时类型两个概念,何时出现多态

多态性:面向对象中最重要的概念。具体就是父类的引用指向子类的对象,可以直接应用在抽象类和接口上。

引用变量的两个类型:编译时类型与运行时类型。简而言之编译时看左,运行时看右。

  • 编译时类型:由声明该变量时使用的类型决定。
  • 运行时类型:由实际赋给该变量的对象决定。

当编译时类型与运行时类型不一致时,就出现了对象的多态性(Polymorphism)。在多态情况下,看左边就是父类的引用(此时父类中不具备子类特有的方法),看右边是子类的对象(实际运行的是子类重写父类的方法)。


多态与非多态情况下不同引用的区别:引用《head first java》

image-20210128111511973

  • 非多态情况下创建自己本身实例会有所有的掌控权;多态情况下只能掌握其左边引用的声明类。


虚拟方法与动态绑定

虚拟方法调用(Virtual Method Invocation)

虚拟方法:若子类定义了与父类同名同参数的方法,多态情况下,将父类中的方法都成为虚拟方法。父类根据赋给它的不同子类对象,会动态的调用属于子类的该重写方法,这样的方法调用在编译器无法确定,在运行时才能确定。

动态绑定:编译时使用的是父类引用,而方法的调用是在运行时确定的,并且调用的是子类重写的方法。

public class Main {
    public static void main(String[] args){
        Person stu = new SmallStu();
        stu.introduce();//i am SmallStu的方法
    }
}

class Person{
    public void introduce(){
        System.out.println("i am Person的方法");
    }
}

class SmallStu extends Person{

    @Override
    public void introduce() {
        System.out.println("i am SmallStu的方法");
    }
}

编译时stu是指向父类引用,运行时会看右边的子类对象,当调用其方法时编译期间只会去找父类引用的方法,运行时会调用子类重写的方法,所以结果是调用的子类的方法。



行为描述及场景使用

对于多态一定是运行时行为。多态时在编译期时只能调用引用父类的方法,若是调用的方法是对应子类对象重写的,会在运行期间确定并调用。

场景示例如下

设置了两个子类分别继承了父类并重写方法

//父类Person
class Person{
    public void introduce(){
        System.out.println("i am Person");
    }
}
//子类
class ChangLu extends Person{

    @Override
    public void introduce() {
        System.out.println("i am ChangLu");
    }
}
//子类
class LinEr extends Person{
    @Override
    public void introduce() {
        System.out.println("i am LinEr");
    }
}
public class Main {
    public static void main(String[] args){
        introduceSelf(new Person());
        introduceSelf(new ChangLu());
        introduceSelf(new LinEr());
    }

    //这个方法主要调用其参数中的方法
    public static void introduceSelf(Person person){
        person.introduce();
    }
}

image-20210126153047178

introduceSelf方法会在调用时输出传入参数的不同身份,这里可以使用多态,将父类引用变量作为参数,那么不同的子类对象传入进行来时都会在运行期间调用其自己本身的方法。


多态还有其他使用场景,例如数据库连接方法中,定义参数为一个连接类父类,那么传入进行的对象若是不同的继承其连接类父类的子类,就能够根据不同数据库进行连接操作。



重载与重写(早、晚绑定)

这里描述的是重载与重写关于早绑定与晚绑定

重载:就是一个类中可以存在多个同名方法,只要其参数不同即可形成重载。编译器对具有相同方法名称但不同参数列表的方法名称会进行修饰。在编译器眼中我们看起来相同名称的重载方法就成了不同的方法,其调用地址在编译时期就进行绑定。

  • 重载可以包含父类和子类的,也就是说子类可以重载父类的同名不同参数的方法。
  • 编译期间即可确定方法称为早绑定或静态绑定

重写:针对于父子类继承关系的类才会有重写方法的机会,重写的是继承父类同名同参数的方法,若是定义引用变量是多态情况下,编译期调用的是父类方法,而真正运行期会去调用其重写的方法。

  • 运行期间确定调用方法称为晚绑定或动态绑定

主要不是晚绑定就不是多态!!!



Instanceof(包含向下转型介绍)

前言描述

例如:A是B的父类,A b = new B();

进行多态后,实际上内存中还是加载了B的属性,在使用多态时只对方法有效,对属性无效,并且调用的也只能是父类的属性。

举个简单的例子吧,A中有B相同的属性及方法,然后进行了上面的多态,你输出该属性以及调用方法(子父类都相同给的),最终结果:属性是父类的,方法是子类重写的

  • 由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性与方法,子类特有的属性和方法不能调用。

如何在多态后调用子类特有的属性及方法呢?instanceof出现

多态A a = new B();

可以通过向下转型或称作多态的方式:B b = (B)a;

注意:需要有子父类关系的情况下才能够进行向下转型。对于不是子父类关系强转编译时就会报错。若是有子父类关系的将原本父类转为子类的编译时不会报错,运行时会报异常(java.lang.ClassCastException)。

//情况一:将两个没有子父类关系的进行强转,编译时就会有异常
Dog d = new Dog();
Person p = (Person)d;

//情况二:运行时异常,父类向下转子类(非多态)
//Person父类,boy是继承Person的子类
Person p = new Person();
Boy b = (Boy)p;//会报转换异常java.lang.ClassCastException

问题出来了我们就需要解决,为了避免向下转型出现异常,通过进行a instanceof B的判断返回true或false来判定是否可以进行向下转型。

语法:例如a instanceof B,a部分必须是实例,B才是类,判断a是否是B的实例。

实际应用:方法参数是多态情况下进行判断向下转型获取值

//为了测试将属性都设置为了public,用于进行向下转型打印属性
class Person{
    public String name = "person";
}

class Boy extends Person{
    public String name = "Boy";
}

class Girl extends Person{
    public String name = "Girl";
}

public class Main {
    public static void main(String[] args){
        introduceSelf(new Person());
        introduceSelf(new Boy());
        introduceSelf(new Girl());
    }

    //打印每个人的各自名称()
    public static void introduceSelf(Person person){
        //判断是否是Boy的实例,切勿出现这种情况:Person person = new Girl();  Boy boy = (Girl)person;
        if(person instanceof Boy){
            Boy boy = (Boy)person;
            System.out.println(boy.name);
        }//判断是否是Boy的实例,切勿出现这种情况:Person person = new Boy();  Girl girl = (Girl)person;
        else if(person instanceof Girl){
            Girl girl = (Girl)person;
            System.out.println(girl.name);
        }else{
            System.out.println(person.name);
        }
    }   
}

image-20210126163355171

这边再添加一个案例,对于向下转型的对象的内存地址依旧是转型前的内存地址

//Boy是Person的子类
Person person = new Boy();
System.out.println(person);//Boy@677327b6
Boy boy = (Boy) person;
System.out.println(boy);//Boy@677327b6


四、Object超类

Object:Java中所有类的始祖,也就是祖宗(根父类),每个类都是由它扩展而来的,包括数组的父类也是Object。如果在类中声明未使用extends关键字指明父类,就默认父类为java.lang.Object类。

Objcet类结构预览图:所有类都具有了其Object的方法,Object无属性,其并不是抽象类,可以进行实例化,只有一个空参构造器,其中部分方法是final类型,子类无法重写。

image-20210125211134494

问1:Object不是抽象类,其是具体的,为什么会允许有人去创建Object对象?是不是不合理?

  • 因为有时候就会会需要一个通用的对象,一个轻量化的对象。最常见的用途是用来线程的同步化上。

问2:Object主要目的是提供多态的参数与返回类型吗?

  • ①作为多态让方法可以应付多种类型的机制。
  • ②提供Java在执行期对任何对象都有需要的方法。
  • ③有一部分与线程相关。

问3:既然多态这么有用,为什么不把所有参数与返回类型都设置为Object?

  • java对于保护程序代码有一项重要机制"类型安全检查",使用多态时再编译期间只会将左边声明类作为实例,这也代表你只能调用该类的方法。例如:Object o = new Person(); o.walk(); 那么第二条调用方法无法通过编译,因为walk()在Object中并没有,编译期间只会认定Object作为实例。
  • Java是类型检查很强的语言,编译器会检查你调用的是否是该对象确实可以响应的方法。


1、==运算符与equals方法

==运算符

==运算符:可使用于基本数据类型变量与引用数据类型变量

  • 基本数据类型变量:比较两个保存的字面值是否相等(不一定类型相同,如整型、浮点型)
  • 引用数据类型变量:比较两个对象的地址值是否相等。实际上就是两个引用地址是否是在堆中同一个开辟的对象(即在堆中的地址)。

比较特殊举例:

//基本数据类型:整型与浮点型比较
int a = 1;
int b = 1.0;
System.out.println(a == b);//true

//引用类型:创建对象的String字符串   堆中开辟的内存地址不同,所以为false
String str = new String("changlu");
String str1 = new String("changlu");
System.out.println(str == str1);//fasle

//引用类型:直接赋值的String字符串  直接赋值的是存于方法区中的常量池里,相同的字符串会引用同样的地址,不会再重新开辟一份空间
String str2 = "changlu";
String str3 = "changlu";
System.out.println(str2 == str3);//true

equals方法

equals:仅适用于引用数据类型

每个类都会有默认的equals方法(因为都继承了Object类),看一下Object类中equals方法:

public boolean equals(Object obj) {
    return (this == obj);
}

在Object中的equals方法与==的作用相同,比较两个引用地址值是否相同

而实际上我们对于对象的比较应该是对其中类的属性进行比较,所以一般都会重写equals方法,在String,Date,File,包装类中实际上重写了Object类中的equals方法,这些实际上都是比较其实体内容是否相等。

看一下String类中重写的equals方法:

public boolean equals(Object anObject) {
	//首先看是否是一个地址的引用
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}


2、toString()方法

toString():用来描述我们对象的信息

首先看一下Object中的toString()方法:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

其中的内存对象地址是虚拟地址,在操作系统之上盖了一层JVM,JVM相当于虚拟的操作系统,所以说自定义类(没重写equals方法的)输出的是虚拟的地址,而不是真实的内存地址。

我们之前使用System.out.println()输出一个自定义对象时,输出的是一个如Boy@677327b6,实际上就与这个Object的toString()有关,看一下源码:

//System源码:System.out =》PrintStream out = null;
public void println(Object x) {
    //调用了String的valueOf()
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

//String源码:valueOf()方法
public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

我们可以看到通过println()输出的实际上就是自定义对象toString()方法,valueOf()方法中采用了多态,由于我们自定义类没有重写toString()方法,所以会默认调用Object中的进行输出也就是一个虚拟地址值了。

toString()自定义如下:

class Person{
    public String name = "person";

    //这是IDEA自动生成的toString,我们也可以修改成自己想要输出的形式
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

public class Main {
    public static void main(String[] args){
        System.out.println(new Person());//Person{name='person'}
    }
}

通过自定义toString()方法,我们再次输出就不再是一个虚拟地址值了(并不是说明栈中引用变量不再是堆中的内存地址了),可以输出类中自定义内容。



五、包装类

介绍拆装箱与转换问题

包装类:针对于八种基本类型定义相应的引用类型,也称为封装类。

基本数据类型 包装类
byte Byte
boolean Boolean
short Short
char Character
int Integer
long Long
float Float
double Double

JDK5.0之后引入了装箱与拆箱的概念。

  • 装箱操作:Integer i = 15; 隐藏了Integer.valueOf(15)方法,实际上也是通过new 对象获得的对象实例。
  • 拆箱操作:int i1 = i; 隐藏了Integer.intValue()方法,返回int值。

针对于包装类如何创建实例?可直接赋值或者通过有参构造器。

  • 一般直接赋值,如Integer i = 1;,或者通过有参构造器 Integer i = new Integer(100);
  • 比较特殊的是Boolean类提一下,其通过构造器创建实例如Boolean boolean = new Boolean("tRue");,其中true与false可以不区分大小写,其他有误。

基本数据类型与包装类转换

  • 包装类 =》基本数据类型:xxxValue()
  • 基本数据类型 =》包装类:valueOf(xxx)

基本数据类型、包装类与String类转换

  • 基本、包装 =》String类:String.valueOf(xxx)
  • String类 =》基本数据类型:对应包装类.parsexxx()


Integer包装类容易混淆情况

jdk1.5之后,Integer与int之间能够进行自动拆箱与装箱。

Integer中的缓存数组问题

先看这个例子

@Test
public void test02(){
    Integer i = 10;
    Integer j = 10;
    System.out.println(i == j);//true
}
  • 两个Integer包装类实例直接赋值10,中间会有装箱的过程会自动调用Integer.valueOf()方法,此时就会有一个疑问抛出,为什么我们声明的是两个引用类型,最后==判断引用地址问什么相同

针对于该问题我们应当去Integer源码中一探究竟:

//包装类源码
public final class Integer extends Number implements Comparable<Integer> {
    
    //装箱过程会调用valueOf()方法,其中会对i进行判断是否在[-128,127]中,若是的话直接返回一个cache[xx]指定下标值,
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            //若是10,则返回cache[138]=>10
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    //静态内部类IntegerCache:用于预先存储[-128,127]的所有值到Integer数组cache中,数组中都是一个个Integer实例
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            //cache[0]赋值-128向上累加
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
}

总结:在进行装箱过程中使用的valueOf(int i)方法,其中i若是在[-128-127]区间中,那么返回就是就是cache数组中已经保存好的Integer实例(初始化类时保存),所以上面的程序结果为true,都是在cache数组中同一个实例。


new Integer()案例

@Test
public void test02(){
    Integer i = new Integer(10);
    Integer j = new Integer(10);
    System.out.println(i == j);//false
}

注意:这里是new的实例,在堆中是各自独立开辟出来的对象实例,所以结果为false,并没有调用valueOf()这个方法。

看下源码

public Integer(int value) {
    this.value = value;
}
  • 在Integer包装类中使用final int value来存储数据的。


相关面试题

1.三元运算符:若是包含三元运算符,会进行自动类型提升,例如下面的Integer会提升为Double,最后输出1.0。

Object o = true?new Integer(1):new Double(2.0);
System.out.println(o);//1.0

2.if-else判断对于自动类型不会提升。

Object o;
if(true){
    o = new Integer(12);
}else{
    o = new Double(12.0);
}
 System.out.println(o);//12


参考文章

[1]. Java创建子类实例的时候也会创建父类实例吗?

[2]. 子类将继承父类所有的属性和方法吗?为什么? 看其回答

[3]. [java]为什么System.in/out/err值为null?

[4]. 书籍《head first java 2.0》



我是长路,感谢你的阅读,如有问题请指出,我会听取建议并进行修正。
欢迎关注我的公众号:长路Java,其中会包含软件安装等其他一些资料,包含一些视频教程以及学习路径分享。
学习讨论qq群:891507813 我们可以一起探讨学习
注明:转载可,需要附带上文章链接

posted @ 2021-02-20 19:55  长路  阅读(44)  评论(0编辑  收藏  举报