面向对象和面向过程

  • 面向过程(Procedure Oriented):把问题分解成一个个步骤,按照步骤调用函数。
    • 自上而下,先定好函数入口,再逐步实现用到的方法
    • 一个类具备各种各样的功能。
    • 代码重用性低、扩展能力差,后期维护困难,代码耦合高。
  • 面向对象(Object Oriented):把属性、行为封装成对象,对同类对象提取共性,形成类,通过不同对象之间的组合、调用解决问题。
    • 自下而上,先设计组件,再通过对象间组合、调用,完成业务逻辑。
    • 每个类只负责自己的属性和功能。
    • 性能比面向过程低

概念解释

  • 声明:告诉编译器有一个变量,不分配存储空间。例如User user;。先声明后赋值,即懒加载。
  • 定义:给变量分配存储空间或赋值。例如User user = new User();
  • 创建:new 类名()的动作即为创建
  • 实例化:创建一个对象也叫实例化一个对象。这个对象就是类的一个实例。
  • 初始化:
    • 对象初始化:创建对象的时候会进行初始化动作,给成员变量赋默认值,执行构造方法,代码块等
    • 变量初始化:即给变量赋值
  • 变量:变量分为基本类型变量和类变量,其中类变量也可以叫做引用
  • 引用:
    • 做名词时(也叫别名):是一个存放对象地址的变量,一个对象可以有多个变量引用。如user变量也叫user对象的引用
    • 做动词时:指让变量指向堆空间的对象地址,简单理解为给变量赋引用类型的值(非基本类型)的过程。例如:给对象添加了xxx引用,一个变量引用了xxx对象
  • 类的对象(Object):也叫类的实例(实例化对象,Instance),对象的类型是该类。对象创建之后存放在堆空间中。
    • new创建
    • 反射获得Class对象,调用newInstance创建
  • 类对象(字节码对象):类加载的时候会为该类创建一个对象,是Class类型,同一个类在一个jvm中只存在一份字节码对象。
    • Class.forName("包名")
    • new Object().getClass()
    • 类名.class
class Person {//Person是类
    //属性、成员变量
    private String name;
    //构造方法
    public Person(String name) {
        this.name = name;
    }
    //方法
    public void say(String str){
        System.out.println(str);
    }
}
//new用来在堆上创建对象,这里的“=”并不是赋值的意思,而是把对象的地址传递给变量
Person person = new Person("张三"); //person是引用(别名),new Person()是真正的对象,没有名字

//引用可以指向不同的对象
person = new Person("李四");

//对象可以有多个引用
Person person1;
Person person2;
person1 = new Person("张三");
person2 = person1;

三大特性

封装、继承、多态

  • 封装:对客观事物进行抽象。将对象的属性和行为封装成一个类,并可以设置属性和方法对外的可见性。
    • 类描述了一类事物的状态和行为,是一种抽象的数据类型。如:把人抽象成一个类,有四肢、五官,可以说话、走路
    • 类是对象的抽象,对象是类的具体(实例)。类可以看作是对象的模板,一个类可以有多个对象。如:人是一个类,一个人是具体对象
  • 继承:可以实现现有类的所有功能,并可以对现有类的功能进行扩展。
    • 继承的类称为子类或派生类,被继承的类称为基类、父类、超类。
    • 父类具有子类的共性,子类拥有父类没有的特性。如:把人当作基类,男人、女人可以作为子类,二者具备不同的特性。
    • 子类中可以通过super使用父类方法
  • 多态:同一个行为在不同情形下可以有不同的表现形式。
    • 方法多态:重写和重载。
    • 对象多态:父类和子类对象间的转化
      • 向上转型:子类对象变为父类对象。父类 变量名称 = 子类实例。(自动转换)
      • 向下转型:父类对象变为子类对象。子类 变量名称 = (子类) 父类实例。(需要强制转换)
    • 参数多态:类定义和方法定义时不指定具体参数类型,把类型作为参数使用,创建实例或调用方法的时候可以指定不同的具体类型。如Java泛型(也叫参数化类型)

注:对方法的覆盖才有多态,对成员变量的覆盖没有多态。构造方法也没有多态,这很容易理解,因为无法重写构造方法。

public class Main {
    static class A {
        String a = "A";
        String getA() {
            return a;
        }
    }
    static class B extends A {
        String a = "B";
        String getA() {
            return a;
        }
    }
    public static void main(String[] args) {
        A obj = new B();
        System.out.println(obj.a); //输出A,成员变量的覆盖没有多态
        System.out.println(obj.getA()); //输出B,方法的覆盖才有多态
    }
}

重写和重载

重载(Overload) 重写(Override)
定义 同一个类中,同一个方法名称,根据不同参数列表,可以完成不同的功能。 父类和子类具有同一个方法,通过操作不同子类对象,可以完成不同的功能。
要求 相同方法名,参数列表不同。即方法签名不同。 具有相同的方法名,参数,返回类型。即方法签名相同
时期 编译时多态:编译时根据方法签名(方法名和参数列表)确定调用的方法 运行时多态:运行时根据变量指向的实际对象确定真正调用的方法
访问修饰符 可以改变访问修饰符 重写方法访问权限不能比父类严格,但是可以比父类更松
抛出异常 可以抛出新的或更广的异常 重写方法抛出异常范围不能比父类大(RuntimeException除外),但是可以比父类异常范围更小或更少
发生类 可以发生在一个类中,也可以发生在父类和子类中 发生在父类和子类中,要求有类继承或接口实现
备注 只有返回值相同不能重载。 final方法无法重写

每一个变量都有两种类型,ClassA obj = new ClassB();

  • 静态类型(编译时类型、引用类型、声明类型):引用变量的类型,在编译期确定,无法改变。即ClassA
  • 动态类型(运行时类型、实际类型、真实类型):实例对象的类型,在编译期无法确定,需要运行期确定,可以改变。即ClassB

继承与实现

继承(Inheritance) 实现(Implement)
定义 如果多个类的某个部分的功能相同,那么可以抽象出一个类,把他们的相同部分都放到父类里,让他们都继承这个类。 如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,让不同的类根据各自的具体逻辑实现这个接口。
目的 继承父类功能,复用代码。父类有具体实现 对多个类都具备的特定的行为定义一个公共标准。不包含具体实现
举例 鸟类会飞,相应的继承鸟类的麻雀、燕子等,也具备飞的功能 飞机和鸟都有飞的行为,但是飞的方式不同,具有不同的实现
关键字 使用extends 使用implementation
备注 抽象类可以定义抽象方法,抽象方法强制要求子类实现,非抽象方法不强制要求 接口方法默认是抽象的,强制要求子类实现。Java8之后接口可以有默认实现,使用default修饰
基类可以定义自己的属性、方法等 接口的属性和方法只能是public的。属性只能是全局常量(static final),方法不能用final修饰
一个类只能继承一个类 一个类可以实现多个接口

补充:

  1. 继承和实现都是描述两个类间的关系,继承关系比实现关系更加紧密:只要两个类有一定的相似性就可以抽取接口,而继承要求子类和父类有大部分共性,子类是由父类衍生出来的。
  2. 继承和实现并不完全对应类和接口。继承关系中,子类也可以有实现。Java8中,接口可以有默认实现,子类可以继承默认实现
  3. 一个接口可以继承多个父接口(不需要实现)。A extends B,C
  4. 子类属性会覆盖父类的同名属性。

多继承带来的问题?

菱形问题:如图,D类继承B类和C类,B类和C类又同时继承A类。D会因为多继承,继承到两份A类的属性和方法。

C++引入了虚继承解决菱形问题。Java类不允许多继承,但是接口支持多继承,并且Java8中接口也可以有默认实现。

如果B和C接口有同名方法,且都有默认实现,则D类需要重写方法。

如果A和B接口有同名方法,且都有默认实现,则使用B接口的默认实现,相当于B接口重写了方法

问题

public class Main {
  public void test() {
    Object s = null;
    foo(s); //编译错误,大类型无法转为小类型,需要强制转换
    foo(null); //编译错误,无法确定调用哪个重载方法
  }
  private void foo(String s) {}
  private void foo(Integer s) {}
}
public class Main {
  public void test() {
    foo(1); //调用double方法。此时不会自动装箱,会进行自动类型转换
  }
  private void foo(double s) {}
  private void foo(Integer s) {}
}

接口和抽象类

接口作用,如何理解面向接口编程?

  1. 对业务逻辑进行抽象
  2. 实现多态
  3. 对外部隐藏细节,不涉及具体实现细节

接口和抽象类都不能直接实例化,需要子类继承和实现。

参数 抽象类 接口
抽象层次 对类的抽象 对行为的抽象
默认的方法实现 可以定义抽象方法和普通方法,抽象方法强制要求子类实现,非抽象方法不强制要求 接口方法默认是完全抽象的,强制要求子类实现。java8可以使用接口方法的默认实现,用default修饰
属性和方法 可以定义自己的属性、方法等 接口的属性和方法只能是public的。属性只能是全局常量(public static final),方法不能用final修饰
使用 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,需要实现所有的抽象方法。 子类使用implements关键字来实现接口。如果子类不是抽象类的话,需要实现所有接口方法
构造器 抽象类可以有构造器 接口不能有构造器
访问修饰符 抽象方法可以有publicprotecteddefault这些修饰符 接口方法默认修饰符是public。不可以使用修饰符。
main方法 抽象方法可以有main方法并且我们可以运行它 接口没有main方法,因此我们不能运行它。(java8以后接口可以有default和static方法,所以可以运行main方法)
多继承 可以继承其他抽象类、实现其他接口 只可以继承其他接口
子类多继承 抽象类不能被多继承 接口可以被多实现,注意避免多个接口定义同名方法
添加新方法 往抽象类中添加新的方法,可以提供默认的实现。不需要修改子类 往接口中添加方法,需要修改实现类
设计层次 抽象类是自底向下抽象而来的 接口是自顶向下设计来的

继承与组合

具体可见设计模式-面向对象基本原则中的合成复用原则

继承 组合
强调对象是什么的关系。即is-a 强调整体与部分拥有的关系,即has-a
不支持动态继承:编译时就确定好两个类的继承关系,运行时无法替换父类 支持动态组合:可以在运行时选择不同类型的组合对象
自动继承父类的功能 不能自动继承父类的功能,需要手动包装方法
创建子类对象的时候,无须创建父类对象 创建整体类对象时,需要先创建局部类对象并传入

多用组合,少用继承

  1. 继承和组合都体现了类的复用性
  2. 组合比继承耦合度低,更灵活,更容易扩展
  3. 两个类确实存在is-a关系的时候才使用继承

类的定义

访问修饰符

Java中有四种访问修饰符,可以修饰类、属性、方法,默认访问修饰符是default。在接口中默认是public,default表示接口默认实现

  1. public:所有类和对象都可访问
  2. protected:同一个包中的类,以及其他包的子类可访问
  3. default:同一个包中的类可以访问,其他包的类不可访问,即使是子类
  4. private:除了当前类可以访问,其他类都不能访问,包括子类

变量

  1. 类变量(静态变量):存储在JVM方法区中,类加载的时候初始化
  2. 成员变量:存储在JVM堆内存中,有默认初始值,创建对象的时候初始化
  3. 局部变量:存储在JVM栈内存中,没有默认初始值,使用之前需要赋值

构造函数(构造方法)

构造函数是特殊的方法,用来创建并初始化对象。

特点:

  1. 没有返回类型
  2. 不能被重写
  3. 名称和所属类的名称相同
  4. 没有声明构造函数的情况下,会生成默认无参构造函数。如果手动定义了一个有参数的构造函数,不会再生成默认无参构造函数
  5. 继承的时候,子类实例化会先调用父类的构造方法,因此子类的构造方法中一定要调用super。如果父类是无参构造函数,编译器会隐式帮我们调用super。
  6. 继承的时候,如果父类声明了有参构造函数,子类一定要定义一个构造函数(可以是有参,也可以是无参),并且需要手动调用父类构造函数,即super(参数列表):不定义的话子类会生成默认无参构造函数,并隐式调用父类无参构造函数,即super()。而父类此时并没有无参构造函数,因此会编译失败。

this和super

this和super使用方式:

  1. 通过this引用自身属性和方法。通过super引用父类属性和方法。
  2. this和super无法在静态环境中使用:包括静态方法、静态变量、静态内部类、静态代码块等
  3. this一般可以缺省,除非有同名的局部变量,无法指代的时候。
  4. 在内部类中(非静态内部类、匿名内部类),需要使用类名.this.属性/方法类名.super.属性/方法进行引用
  5. 构造方法引用不需要.,如this(参数列表),super(参数列表)
  6. 不能在普通方法中使用this和super调用构造方法,只能在其他构造方法中。
  7. 使用this和super调用构造方法的时候,必须放在构造函数的第一行
  8. 在一个构造方法中不能同时调用this和super构造方法:不能调用两次this,不能调用两次super,也不能一次this一次super。
  9. 子类构造方法中会隐式的调用父类无参构造函数,即super()。如果父类没有默认无参构造函数,编译器会报错,需要使用super手动调用有参构造函数。

this和super的主要区别:

子类重写并不会删除父类的方法,只是对子类隐藏了父类的方法,通过super来访问隐藏的父类方法和成员变量。

子类实例化的时候会调用父类的构造函数,但是并不会创建父类对象,而是借用父类的构造方法来创建子类对象。会从父类中继承成员变量并分配空间,但不会给父类对象划分空间。

this指代当前实例对象的引用,super是java提供的关键字,不是父类的实例的引用。

如何证明super不是父类对象的引用? >

  1. 不能将super赋值给变量,但是可以将this赋值给变量
  2. 抽象类不能创建对象,但父类是抽象类的时候,仍然可以通过super调用父类方法
  3. super.test()是调用父类中的test方法,不是说调用父类对象的test方法

子类中的this很好理解,毫无疑问是自身。那么父类中的this指代的是什么?是实际的类型,还是父类自身?

根据上面的结论,实例化子类的时候并不会创建父类对象,因此this不可能是父类对象。那么this永远引用的是子类的元素吗?

  1. this获取对象引用,指代的是实际对象。
  2. this.方法:由于重写方法多态,访问的是子类的方法
  3. this.成员变量:由于成员变量没有多态,因此访问的是父类的变量
  4. this构造方法:由于构造方法没有多态,访问的是父类的其他构造方法
  5. this作为参数:此时this的静态类型是父类自身,无法确定实际类型。由于方法重载是编译时的多态,因此根据静态类型进行分派

总结:this相当于变量,this的静态类型为父类,实际类型为子类。静态分派取决于静态类型,动态分派取决于实际类型

public class Main {
    static class A {
        String a = "A";
        String getA() {
            return a;
        }
        void test() {
            A obj = this; //不需要强转
            B obj2 = (B) this; //需要强转,说明this的声明类型是A
            System.out.println(obj); //输出B对象,相当于调用obj.toString(),方法有多态
            System.out.println(this); //输出B对象,相当于调用this.toString(),方法有多态
            System.out.println(this.a); //输出A,成员变量没有多态
            System.out.println(this.getA()); //输出B,方法有多态
            this.print(this); //输出:B printA:B对象。
            //重写是运行时的多态,因此调用的是B类的print方法
            //重载是编译时的多态,根据静态类型进行分派,此时this的静态类型是A,实际类型无法确定,因此选择的是参数为A的方法
        }
        void print(A obj) {
            System.out.println("A printA:" + obj);
        }
        void print(B obj) { //重载
            System.out.println("A printB:" + obj);
        }
    }
    static class B extends A {
        String a = "B";
        String getA() { //重写
            return a;
        }
        void print(A obj) { //重写
            System.out.println("B printA:" + obj);
        }
        void print(B obj) {
            System.out.println("B printB:" + obj);
        }
    }
    public static void main(String[] args) {
        A obj = new B();
        System.out.println(obj.a); //输出A,成员变量没有多态
        System.out.println(obj.getA()); //输出B,方法有多态
        obj.test();
    }
}

instanceof和isInstance

  • instanceof是关键字,用于判断对象是否属于某个类型
  • isInstance是Class类的方法,用于判断对象能否强转为该类型,内部调用isAssignableFrom实现
  • isAssignableFrom是Class类的方法,用于判断一个类A是否是另一个类B的超类或接口。A.class.isAssignableFrom(B.class)

用法:

Object obj = "abc";
System.out.println(obj instanceof String);//true
System.out.println(obj instanceof Object);//true

System.out.println(String.class.isInstance(obj));//true
System.out.println(Object.class.isInstance(String.class));//true

//对象是null时,都返回false
System.out.println(null instanceof Object); //false
System.out.println(Object.class.isInstance(null)); //false

常用关键字

  1. abstract:只能修饰类和方法,只能在抽象类或接口中使用,接口方法默认是抽象的。
    1. 抽象类不能直接实例化。可以new A() {}实例化,实际上是创建了一个匿名内部类
    2. 抽象方法不能有具体实现,需要由子类实现方法。
    3. 不能和private、final、static、synchronized、native共存。修饰静态内部类的时候可以和static共存。(native是要求原生实现,abstract是要求子类实现)
  2. final表示不可修改。
    1. 修饰变量:表示不可修改,即常量。对于基本数据类型表示值不可改变,对于引用数据类型,表示引用地址不可改变
    2. 修饰类:表示不可被继承
    3. 修饰方法:表示不可被重写。
  3. static表示静态:修饰成员变量、方法、内部类、代码块
    1. 和类绑定,不和对象实例绑定。
    2. 只在类首次加载的时候初始化,在内存中只有一个副本,存储在方法区中。
    3. 静态方法或静态代码块中,不能使用非静态变量。(非静态变量需要实例化对象,而静态方法不需要实例化对象就可以使用)
    4. 需要使用类名.进行引用,直接使用对象引用会有警告。反编译之后发现
//定义一个A类和一个静态方法
//源代码如下
new A().funStatic();
//反编译结果如下
new A();
A.funStatic();

数组定义和初始化

  • 动态初始化:先声明数组大小,之后赋值,由系统分配默认值。基本数据类型使用各自的默认值,引用数据类型(包括String)默认值为null
  • 静态初始化数组时,不必指明长度: int[] a = {1, 2, 3}
  • “[]” 是数组运算符的意思,在声明一个数组时,数组运算符可以放在数据类型与变量之间,也可以放在变量之后。
  • 数组长度只能用short或int限定,否则会编译错误,如char[] a = new char[1L]
  • 定义多维数组时,其一维数组的长度必须首先指明,其他维数组长度可以稍后指定;

二维数组声明可以如下:

int a[][] = new int[10][10];
int []b[] = new int[10][10];
int [][]c = new int[10][10];
//多维数组可以先声明第一维长度,后面的长度可以稍后声明
int d[][] = new int[10][];

面向对象设计基本原则

SOLID原则+迪米特法则+合成复用原则,具体解释看设计模式-面向对象基本原则

  • 单一职责(SRP,Single-Responsibility Principle):一个类只做一件核心的事,只有一个引起它变化的原因
  • 开闭原则(OCP,Open-Closed Principle):对扩展开放,对修改关闭
  • 里氏替换原则(LSP,Liskov-Substitution Principle):任何使用父类的地方,都能够被其子类替换,并且不影响程序运行结果。
  • 接口隔离原则(ISP,Interface-Segregation Principle):将臃肿庞大的接口拆分成多个专门的小接口,让接口只包含客户感兴趣的方法。
  • 依赖倒置原则(DIP,Dependency-Inversion Principle):高层模块不依赖于底层模块,二者都依赖于抽象。抽象不应该依赖具体,具体依赖于抽象
  • 迪米特法则(LoD,Law of Demeter):也叫做最少知识原则。一个对象对其他对象应当尽可能少的了解。
  • 合成复用原则(CRP,Composite Reuse Principle):也叫组合/聚合复用原则。简单来说就是多组合,少继承。

结语

参考文章:

results matching ""

    No results matching ""