Java面向对象编程

Java面向对象编程

一、面向对象的三大特性

  1. 封装

    其核心思想就是隐藏不需要对外公开的属性和方法,以减少对外耦合,包证数据的安全和程序的稳定。例如:通常使用private修饰成员变量,提供对应的getter、setter方法,外部只能通过方法来控制成员变量;同时把代码用方法进行封装,提高代码的复用性。

    Java访问权限关键字:

    本类内部本包子类外部包
    public
    protected×
    default××
    private×××

    说明:Java访问权限控制是在编译层,不会再类文件中留下痕迹。通过反射机制,还是可以访问类的私有成员。

  2. 继承

    多个类共同的成员变量和成员方法,抽取到另外一个类中(父类),再让多个类去继承这个父类。

    特点:

    1. 子类继承父类的成员变量

      当子类继承了某个类之后,便可以调用父类中的成员变量,但是并不是完全继承父类的所有成员变量。规则如下:

      1) 能够访问父类的public和protected成员变量;不能访问父类的private成员变量(只是拥有,不能使用,可以通过反射获取);

      2) 对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能继承;否则,不能继承;

      3) 对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名的成员变量,需要使用super关键字来进行引用。

    2. 子类继承父类的方法

      1) 能够访问父类的public和protected成员方法;不能访问父类的private成员方法(只是拥有,不能访问,可以通过反射访问);

      2) 对于父类的包访问权限的成员方法,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能继承;

      3) 对于子类可以继承的父类的成员方法,如果在子类中出现了同名称和参数的成员方法,则是重写,即子类的成员方法会覆盖父类的成员方法(父类的成员方法如果是静态的,子类也必须的静态的才能覆盖,不然编译错误)。如果要在子类中访问父类中同名的成员方法,需要使用super关键字来进行引用。

      public class Parent {
      
          private String name;
      
          private void setName(String name) {
              this.name = name;
          }
      
          private String getName() {
              return "hello!" + this.name;
          }
      }
      public class Child extends Parent {
      }
      public class Client {
      
          public static void main(String[] args) throws Exception {
              Child child = new Child();
              Class<?> clazz = child.getClass();
              Class<?> superClazz = clazz.getSuperclass();
              Method superMethod = superClazz.getDeclaredMethod("setName", String.class);
              // 取消访问检查
              superMethod.setAccessible(true);
              superMethod.invoke(child, "张三");
              superMethod = superClazz.getDeclaredMethod("getName");
              superMethod.setAccessible(true);
              Object returnValue = superMethod.invoke(child);
              System.out.println(returnValue);
      
              Field[] fields = superClazz.getDeclaredFields();
              for (Field field : fields) {
                  System.out.println(field.getName());
              }
          }
      }
      
      输出结果:
      hello!张三
      name
      
    3. 构造器

      子类不能继承父类的构造器,但是如果父类有带参构造器,并且没有默认构造器,那么子类必须有同类型参数构造,如果父类是无参构造,那么子类任意构造都可以。

  3. 多态

    父类的引用指向子类对象:父类类型 引用名称 = new 子类类型

    1. 多态成员变量

      当子类中出现同名的成员变量时,多态调用该变量时:

      编译时期:参考的是引用型变量所属的类中是否有被调用的成员变量。没有,编译失败。

      运行时期:也是调用引用型变量所属的类中的成员变量。

      也就是编译和运行都参考等号的左边

      public class Parent {
          public String name = "parent";
      }
      public class Child extends Parent {
          public String name = "child";
      }
      public class Test {
          public static void main(String[] args){
              Parent child = new Child();
              System.out.println(child.name);
          }
      }
      
      输出结果:
      parent
      
    2. 多态成员方法

      编译时期:参考引用变量所属的类,如果类中没有调用的方法。编译失败。

      运行时期:参考引用变量所指的对象所属的类,并运行对象所属类中的成员方法。

      也就是编译看左边(父类是否拥有该方法),运行看右边(静态都看左边)

      public class Parent {
          public void hello() {
              System.out.println("hello!parent");
          }
          public static void hi() {
              System.out.println("hi!parent");
          }
      }
      public class Child extends Parent {
          @Override
          public void hello() {
              System.out.println("hello!child");
          }
          public static void hi() {
              System.out.println("hi!child");
          }
      }
      public class Test {
          public static void main(String[] args){
              Parent child = new Child();
              child.hello();
              child.hi();
          }
      }
      
      输出结果:
      hello!child
      hi!parent
      

二、对象的分类

  1. 类(抽象类)

    如果一个类含有抽象方法,则这个类一定是抽象类(不包含抽象方法,但被abstract修饰的也是抽象类),抽象类必须在类前使用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。

    抽象类和普通类的主要区别:

    1)抽象方法必须为public或者protected(如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。

    2)抽象类有构造方法,但不能用来创建对象。

    3)如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为抽象类。

  2. 内部类

    为什么需要内部类:无论该内部类的外部类实现或者继承了什么都不会对它造成影响。

    1)成员内部类,定义在外部类中的成员位置。与类中的成员变量相似,可通过外部类对象进行访问

    public class Outer {
    
        class Inner {
            public void hi(String name) {
                System.out.println("hi!" + name);
            }
        }
    
        static class Inner2 {
            public void say(String name) {
                System.out.println("hello!" + name);
            }
        }
    
        public static void main(String[] args){
            // 非静态情况
            Outer.Inner inner = new Outer().new Inner();
            inner.hi("李四");
    		// 静态
            Outer.Inner2 inner2 = new Outer.Inner2();
            inner2.say("王五");
        }
    }
    
    输出结果:
    hi!李四
    hello!王五
    

    2)局部内部类,定义在外部方法的局部位置。与访问方法中的局部变量相似,可以通过调用方法进行方法

    public class Outer {
    
        public void say(String name) {
            class Inner {
                public void hi(String name) {
                    System.out.println("hi!" + name);
                }
            }
            new Inner().hi(name);
        }
    
        public static void main(String[] args){
            Outer outer = new Outer();
            outer.say("赵六");
        }
    }
    
    输出结果:
    hi!赵六
    

    3)匿名内部类,是创建某个类型子类对象的快捷方法

    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("....");
        }
    });
    
  3. 接口

    接口中可以含有变量和方法。但是,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰编译会报错),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、final等修饰编译会报错),并且接口中的所有方法不能有具体的实现(除default方法,java8中接口可以有default方法),也就是说,接口中的方法必须都是抽象方法或default方法。可以看出接口是一种极度抽象的类型,它比抽象类更“抽象”,并且一般情况下不在接口中定义常量。

    接口与抽象类的区别:

    1. 语法层面上的区别

      1)抽象类可以提供成员方法的实现细节,而接口中只能存在抽象方法或默认方法;

      2)抽象类中的成员变量可以是各种类型的,而接口中只能存在常量;

      3)接口中不能含有静态代码块、构造代码块以及普通方法,而抽象类可以有静态代码块、构造代码块和静态方法;

      4)一个类只能继承一个抽象类,而一个类可以实现多个接口

    2. 设计层面上的区别

      1)抽象类是对一种事物的抽象,包括属性和行为,但是接口却仅仅是对局部(行为)进行抽象。

      2)设计层面不同,抽象类作为很多子类的父类,它是一种模板设计。而接口是一种行为规范,更多的是表述不同的行为。

三、类的实例化过程

类的实例化过程如下:

  1. JVM读取指定classpath路径下的class文件,加载到内存,如果有直接父类,也会加载父类;

  2. 堆内存分配空间;

  3. 执行父类、子类静态代码块;

  4. 对象属性进行默认初始化;

  5. 调用构造方法;

  6. 在构造方法中,先调用父类构造方法初始化父类数据;

  7. 初始化父类数据后,显示初始化,执行子类的构造代码块;

  8. 再进行子类构造方法的特定初始化;

  9. 初始化完毕后,将地址赋值给引用。

    class Parent {
    
        int num = 5;
    
        static {
            System.out.println("父类静态代码块");
            System.out.println();
        }
    
        {
            System.out.println("父类构造代码块1," + num);
            num = 1;
            System.out.println("父类构造代码块2," + num);
            doSomething();
            System.out.println();
        }
    
        Parent() {
            System.out.println("父类构造方法1," + num);
            num = 2;
            System.out.println("父类构造方法2," + num);
            doSomething();
            System.out.println();
        }
    
        void doSomething() {
            System.out.println("父类doSomething方法1," + num);
            num = 3;
            System.out.println("父类doSomething方法2," + num);
            System.out.println();
        }
    }
    class Child extends Parent {
    
        int num = 10;
    
        /*
            静态代码块,在类加载时执行
        */
        static {
            System.out.println("子类静态代码块");
            System.out.println();
        }
    
        /*
            构造代码块
        */
        {
            System.out.println("子类构造代码块1," + num);
            num = 11;
            System.out.println("子类构造代码块2," + num);
            doSomething();
            System.out.println();
        }
    
        Child() {
            System.out.println("子类构造方法1," + num);
            num = 12;
            System.out.println("子类构造方法2," + num);
            doSomething();
            System.out.println();
        }
    
        @Override
        void doSomething() {
            System.out.println("子类doSomething方法1," + num);
            num = 13;
            System.out.println("子类doSomething方法2," + num);
            System.out.println();
        }
    
    }
    public class Test {
    
        public static void main(String[] args) {
            Child child = new Child();
            child.num = 20;
            child.doSomething();
        }
    }
    
    输出结果:
    父类静态代码块
    
    子类静态代码块
    
    父类构造代码块15
    父类构造代码块21
    子类doSomething方法10
    子类doSomething方法213
    
    
    父类构造方法11
    父类构造方法22
    子类doSomething方法113
    子类doSomething方法213
    
    
    子类构造代码块110
    子类构造代码块211
    子类doSomething方法111
    子类doSomething方法213
    
    
    子类构造方法113
    子类构造方法212
    子类doSomething方法112
    子类doSomething方法213
    
    
    子类doSomething方法120
    子类doSomething方法213
    

四、面向对象的设计原则

  1. 单一职责原则

    单一职责原则的定义是就一个类而言,应该仅有一个引起他变化的原因。也就是说一个类应该只负责一件事情。如果一个类负责了方法M1,方法M2两个不同的事情,当M1方法发生变化的时候,我们需要修改这个类的M1方法,但是这个时候就有可能导致M2方法不能工作。这个不是我们期待的,但是由于这种设计却很有可能发生。所以这个时候,我们需要把M1方法,M2方法单独分离成两个类。让每个类只专心处理自己的方法。

    单一职责原则的好处如下:

    可以降低类的复杂度,一个类只负责一项职责,这样逻辑也简单很多 提高类的可读性,和系统的维护性,因为不会有其他奇怪的方法来干扰我们理解这个类的含义 当发生变化的时候,能将变化的影响降到最小,因为只会在这个类中做出修改。

  2. 开闭原则

    开闭原则和单一职责原则一样,是非常基础而且一般是常识的原则。开闭原则的定义是软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是关闭的。

    当需求发生改变的时候,我们需要对代码进行修改,这个时候我们应该尽量去扩展原来的代码,而不是去修改原来的代码,因为这样可能会引起更多的问题。

    这个准则和单一职责原则一样,是一个大家都这样去认为但是又没规定具体该如何去做的一种原则。

    开闭原则我们可以用一种方式来确保他,我们用抽象去构建框架,用实现扩展细节。这样当发生修改的时候,我们就直接用抽象派生的一个具体类去实现修改。

  3. 里式替换原则

    里氏替换原则是一个非常有用的一个概念。他的定义如下:

    如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。

    简单的定义如下:

    所有引用基类的地方必须能够透明地使用其子类的对象。

    里氏替换原则通俗的去讲就是:子类可以去扩展父类的功能,但是不能改变父类原有的功能。他包含以下几层意思:

    • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
    • 子类可以增加自己独有的方法。
    • 当子类的方法重载父类的方法时候,方法的形参要比父类的方法的输入参数更加宽松。
    • 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。

    里氏替换原则之所以这样要求是因为继承有很多缺点,他虽然是复用代码的一种方法,但同时继承在一定程度上违反了封装。父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。这也导致了,如果需求变更,子类对父类的方法进行一些复写的时候,其他的子类无法正常工作。所以里氏替换法则被提出来。

    确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建立规范,然后用实现去扩展细节,这个是不是很耳熟,对,里氏替换原则和开闭原则往往是相互依存的。

  4. 依赖倒置原则

    依赖倒置原则指的是一种特殊的解耦方式,使得高层次的模块不应该依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。 这也是一个让人难懂的定义,他可以简单来说就是:

    高层模块不应该依赖底层模块,两者都应该依赖其抽象 抽象不应该依赖细节 细节应该依赖抽象

    在Java 中抽象指的是接口或者抽象类,两者皆不能实例化。而细节就是实现类,也就是实现了接口或者继承了抽象类的类。他是可以被实例化的。高层模块指的是调用端,底层模块是具体的实现类。在Java中,依赖倒置原则是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口是来实现的。这就是俗称的面向接口编程。

  5. 接口隔离原则

    接口隔离原则的定义是:

    客户端不应该依赖他不需要的接口

    换一种说法就是类间的依赖关系应该建立在最小的接口上。这样说好像更难懂。我们通过一个例子来说明。我们知道在Java中一个具体类实现了一个接口,那必然就要实现接口中的所有方法。如果我们有一个类A和类B通过接口I来依赖,类B是对类A依赖的实现,这个接口I有5个方法。但是类A与类B只通过方法1,2,3依赖,然后类C与类D通过接口I来依赖,类D是对类C依赖的实现但是他们却是通过方法1,4,5依赖。那么是必在实现接口的时候,类B就要有实现他不需要的方法4和方法5 而类D就要实现他不需要的方法2,和方法3。这简直就是一个灾难的设计。

    所以我们需要对接口进行拆分,就是把接口分成满足依赖关系的最小接口,类B与类D不需要去实现与他们无关接口方法。比如在这个例子中,我们可以把接口拆成3个,第一个是仅仅由方法1的接口,第二个接口是包含2,3方法的,第三个接口是包含4,5方法的。 这样,我们的设计就满足了接口隔离原则。

    以上这些设计思想用英文的第一个字母可以组成SOLID ,满足这个5个原则的程序也被称为满足了SOLID准则。

  6. 迪米特原则

    迪米特原则也被称为最小知识原则,他的定义是:

    一个对象应该对其他对象保持最小的了解。

    因为类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,所以这也是我们提倡的软件编程的总的原则:低耦合,高内聚。 迪米特法则还有一个更简单的定义:

    只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现在成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

    这里我们可以用一个现实生活中的例子来讲解一下。比如我们需要一张CD,我们可能去音像店去问老板有没有我们需要的那张CD,老板说现在没有,等有的时候你们来拿就行了。在这里我们不需要关心老板是从哪里,怎么获得的那张CD,我们只和老板(直接朋友)沟通,至于老板从他的朋友那里通过何种条件得到的CD,我们不关心,我们不和老板的朋友(陌生人)进行通信,这个就是迪米特的一个应用。说白了,就是一种中介的方式。我们通过老板这个中介来和真正提供CD的人发生联系。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值