Java 学习笔记:第五章 Java面向对象进阶

5.1.1 继承的实现

继承让我们更加容易实现类的扩展。比如,我们定义了人类,在定义Boy类就只需要扩展人类即可。实现了代码的重用,不用再重新发明轮子。

从英文字面意思理解,extends 的意思是“扩展”。子类是父类的扩展。现实世界中的继承无处不在。

在这里插入图片描述
上图中,哺乳动物继承了动物。意味着,动物的特性,哺乳动物都有;在我们编程中,如果重新定义一个Student类,发现已经有Person类包含了我们需要的属性和方法,那么Student类只需要继承Person类即可拥有Person类的属性和方法。

【示例5-1】使用extends实现继承

public class Test{
    public static void main(String[] args) {
        Student s = new Student("高淇",172,"Java");
        s.rest();
        s.study();
    }
}
class Person {
    String name;
    int height;
    public void rest(){
        System.out.println("休息一会!");
    }  
}
class Student extends Person {
    String major; //专业
    public void study(){
        System.out.println("在尚学堂,学习Java");
    }  
    public Student(String name,int height,String major) {
        //天然拥有父类的属性
        this.name = name;
        this.height = height;
        this.major = major;
    }
}

执行结果如图所示:
在这里插入图片描述

5.1.2 instanceof 运算符

instanceof 是二元运算符,左边是对象,右边是类;当对象是右面类或子类所创建对象时,返回true;否则,返回false。比如:

public class Test{
    public static void main(String[] args) {
        Student s = new Student("高淇",172,"Java");
        System.out.println(s instanceof Person);
        System.out.println(s instanceof Student);
    }
}

两条语句的输出结果都是true。

5.1.3 继承使用要点

  1. 父类也称作超类、基类、派生类等。
  2. Java中只有单继承,没有像 C++ 那样的多继承。多继承会引起混乱,使得继承过于复杂,系统难于维护。
  3. Java 中类没有多继承,接口有多继承。
  4. 子类继承父类,可以得到哦父类的全部属性和方法(除了父类的构造方法),但不见得可以直接访问。
  5. 如果定义一个类时,没有调用 extends ,则它的父类是:java.lang.Object

5.1.4 方法的重写 override

子类通过重写父类的方法,可以用自身的行为替换父类的行为。方法的重写是实现多态的必要条件。

方法的重写需要符合下面的三个要点:

  1. “==”:方法名、形参列表相同。
  2. “<=”:返回值类型和声明异常类型,子类小于等于父类。
  3. “>=”:访问权限,子类大于等于父类。

【示例5-3】方法重写

public class TestOverride {
    public static void main(String[] args) {
        Vehicle v1 = new Vehicle();
        Vehicle v2 = new Horse();
        Vehicle v3 = new Plane();
        v1.run();
        v2.run();
        v3.run();
        v2.stop();
        v3.stop();
    }
}
 
class Vehicle { // 交通工具类
    public void run() {
        System.out.println("跑....");
    }
    public void stop() {
        System.out.println("停止不动");
    }
}
class Horse extends Vehicle { // 马也是交通工具
    public void run() { // 重写父类方法
        System.out.println("四蹄翻飞,嘚嘚嘚...");
    }
}
 
class Plane extends Vehicle {
    public void run() { // 重写父类方法
        System.out.println("天上飞!");
    }
    public void stop() {
        System.out.println("空中不能停,坠毁了!");
    }
} 

执行结果如图所示:

在这里插入图片描述

5.2.1 Object 类基本特性

Object 类是所有 Java 类的根基类,也就意味着所有的Java 对象都拥有 Object 类的属性和方法。如果在类的声明中未使用 extends 关键字指明其父类,则默认继承Object类。

【示例5-4】Object类

public class Person {
    ...
}
//等价于:
public class Person extends Object {
    ...
}

5.2.2 toString 方法

Object 类中定义有 public String toString() 方法,其返回值是 String 类型。Object 类中 toString 方法的源码为:

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

根据如上源码得知,默认会返回 “类名+@+16进制的hashcode”。在打印输出或者用字符串连接对象时,会自动调用该对象的toString()方法。

【示例5-5】 toString() 方法测试和重写toString()方法

class Person {
    String name;
    int age;
    @Override
    public String toString() {
        return name+",年龄:"+age;
    }
}
public class Test {
    public static void main(String[] args) {
        Person p=new Person();
        p.age=20;
        p.name="李东";
        System.out.println("info:"+p);
         
        Test t = new Test();
        System.out.println(t);
    }
}

在这里插入图片描述

5.2.3 == 和 equals 方法

“==” 代表比较双方是否相同。如果是基本类型则表示值相等,如果是引用类型则表示地址相等即是同一个对象

Object 类中定义有:public boolean equals(Object obj)方法,提供定义“对象内容相等”的逻辑。比如,我们在公安系统中认为id 相同的人就是同一个人、学籍系统中认为学号相同的人就是同一个人。

Object 的 equals 方法默认就是比较两个对象的 hashcode ,是同一个对象的引用时返回 true 否则 返回flase。但是,我们可以根据我们自己的要求重写 equals 方法。

【示例5-6】equals 方法测试和自定义类重写 equals 方法

public class TestEquals { 
    public static void main(String[] args) {
        Person p1 = new Person(123,"高淇");
        Person p2 = new Person(123,"高小七");     
        System.out.println(p1==p2);     //false,不是同一个对象
        System.out.println(p1.equals(p2));  //true,id相同则认为两个对象内容相同
        String s1 = new String("尚学堂");
        String s2 = new String("尚学堂");
        System.out.println(s1==s2);         //false, 两个字符串不是同一个对象
        System.out.println(s1.equals(s2));  //true,  两个字符串内容相同
    }
}
class Person {
    int id;
    String name;
    public Person(int id,String name) {
        this.id=id;
        this.name=name;
    }
    public boolean equals(Object obj) {
        if(obj == null){
            return false;
        }else {
            if(obj instanceof Person) {
                Person c = (Person)obj;
                if(c.id==this.id) {
                    return true;
                }
            }
        }
        return false;
    }
}

JDK 提供的一些类,如 String、Date、包装类等,重写了Object 的 equals 方法,调用这些类的 equals 方法。x.equals(y),当x 和 y 所引用的对象是同一类对象且属性内容相等时(并不一定是相同对象),返回 true 否则 返回 false。

5.3 super 关键字

super 是直接父类对象的引用。可以通过super 来访问父类中被子类覆盖的方法或属性。

使用 super 调用普通方法,语句没有位置限制,可以子类中随便调用。

若是构造方法的第一行代码没有显式地调用 super(…)或者 this(…);那么Java默认都会调用 super(),含义是调用父类的无参数构造方法。这里的super()可以省略。

【示例5-7】super 关键字的使用

public class TestSuper01 { 
    public static void main(String[] args) {
        new ChildClass().f();
    }
}
class FatherClass {
    public int value;
    public void f(){
        value = 100;
        System.out.println ("FatherClass.value="+value);
    }
}
class ChildClass extends FatherClass {
    public int value;
    public void f() {
        super.f();  //调用父类对象的普通方法
        value = 200;
        System.out.println("ChildClass.value="+value);
        System.out.println(value);
        System.out.println(super.value); //调用父类对象的成员变量
    }
}

在这里插入图片描述

5.3.1 继承树追溯

属性/方法查找顺序:(比如:查找变量h)

  1. 查找当前类中有没有属性h
  2. 依次上溯每个父类,查看每个父类中是否有 h,直到Object
  3. 如果没找到,则出现编译错误
  4. 上面步骤,只要找到 h 变量,则这个过程停止。

构造方法嗲用顺序

构造方法第一句总是:super(…)来调用父类对应的构造方法。所以,流程就是:先向上追溯到 Object,然后再依次向下执行类的初始化调用顺序,与构造方法调用顺序一样,不在重复。

【示例 5-8】构造方法向上追溯执行测试

public class TestSuper02 { 
    public static void main(String[] args) {
        System.out.println("开始创建一个ChildClass对象......");
        new ChildClass();
    }
}
class FatherClass {
    public FatherClass() {
        System.out.println("创建FatherClass");
    }
}
class ChildClass extends FatherClass {
    public ChildClass() {
        System.out.println("创建ChildClass");
    }
}

在这里插入图片描述

5.4.1 封装的作用与含义

我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来,只给我们暴露点单的接口,比如:电源开关。具体内容是怎么实现的,我们不需要操心。

在这里插入图片描述
需要让用户知道的才暴露出来,不需要让用户知道的全部隐藏起来,这就是封装。说的专业一点,封装就是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。

我们程序设计要追求 “高内聚,低耦合”。高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合是仅暴露少量的方法给外部使用,尽量方便外部调用。

编程中封装的具体优点:

  1. 提高代码的安全性
  2. 提高代码的复用性
  3. “高内聚”:封装细节,便于修改内部代码,提高可维护性。
  4. “低耦合”:简化玩不调用,便于调用者使用,便于扩展和协作。

【示例5-9】没有封装的代码会出现一些问题

class Person {
    String name;
    int age;
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
public class Test {
    public static void main(String[] args) {
        Person p = new Person();
        p.name = "小红";
        p.age = -45;//年龄可以通过这种方式随意赋值,没有任何限制
        System.out.println(p);
    }
}

我们都知道,年龄不可能是负数,也不可能超过 130 岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图所示

在这里插入图片描述

再比如说,如果我们哪天我们需要Person 类中的age 属性改为String 类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下 Person 类的 setAge()方法即可,而无需修改使用了该类的客户代码。

5.4.2 封装的实现——使用访问控制符

Java 是使用 “访问控制符” 来控制哪些细节需要封装,哪些细节需要暴露的。Java 中 4种 “访问控制符” 分别为 private、default、protected、public ,他们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。

下面详细讲述它们的访问权限问题。其访问权限范围:
在这里插入图片描述

  1. private 表示私有,只有自己类能访问
  2. default 表示没哟修饰符修饰,只有同一个包的类能访问
  3. protected 表示可以被同一个包的类以及其他包中的子类访问
  4. public 表示可以被该项目的所有包中的所有类访问

下面做进一步说明 Java 中 4种访问权限修饰符的区别:首先我们创建4 个类:Person 类、Student类、Animal 类和 Computer 类、分别比较本类、本包、子类、其他包的区别:

public 访问权限修饰符:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总结:public 修饰符的访问权限为:该项目的所有包中的所有类。

protected 访问权限修饰符:将Person 类中属性改为 protected,其他类不修改

在这里插入图片描述
在这里插入图片描述
总结:protected 修饰符的访问权限为:同一个包中的类以及其他包中的子类。

默认访问权限修饰符:将Person 类中属性改为默认的,其他类不修改

在这里插入图片描述
总结:默认修饰符的访问权限为:同一个包中的类。

private 访问权限修饰符:将Person类中属性改为 private ,其他类不修改。
在这里插入图片描述
总结:private 修饰符的访问权限为:同一个类。

5.4.3 封装的使用细节

类的属性的处理

  1. 一般使用 private 访问权限。
  2. 提供相应的 get/set 方法来访问相关的属性,这些方法通常是 public 修饰的,以提供对属性的赋值与读取操作(注意:boolean 变量的 get 方法是is 开头的)
  3. 一些只用于本类的辅助方法可以用 private 修饰,希望其他类调用的方法用 public 修饰。

【示例5-10】JavaBean 的 封装实例

public class Person {
    // 属性一般使用private修饰
    private String name;
    private int age;
    private boolean flag;
    // 为属性提供public修饰的set/get方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public boolean isFlag() {// 注意:boolean类型的属性get方法是is开头的
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

【示例5-11】 封装的使用

class Person {
    private String name;
    private int age;
    public Person() {
 
    }
    public Person(String name, int age) {
        this.name = name;
        // this.age = age;//构造方法中不能直接赋值,应该调用setAge方法
        setAge(age);
    }
     
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setAge(int age) {
        //在赋值之前先判断年龄是否合法
        if (age > 130 || age < 0) {
            this.age = 18;//不合法赋默认值18
        } else {
            this.age = age;//合法才能赋值给属性age
        }
    }
    public int getAge() {
        return age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
 
public class Test2 {
    public static void main(String[] args) {
        Person p1 = new Person();
        //p1.name = "小红"; //编译错误
        //p1.age = -45;  //编译错误
        p1.setName("小红");
        p1.setAge(-45);
        System.out.println(p1);
         
        Person p2 = new Person("小白", 300);
        System.out.println(p2);
    }
}

执行结果如图所示:

在这里插入图片描述

5.5 多态(polymorphism)

多态指定的是一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。比如:同样是调用人的“休息”方法,张三是睡觉的,李四是旅游;同样是调用人“吃饭”的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

多态的要点:

  1. 多态是方法的多态,不是属性的多态(多态与属性无关)。
  2. 多态的存在要有3 个必要条件:继承、方法重写、父类引用指向子类对象。
  3. 父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。

【示例 5-12】 多态和类型转换测试

class Animal {
    public void shout() {
        System.out.println("叫了一声!");
    }
}
class Dog extends Animal {
    public void shout() {
        System.out.println("旺旺旺!");
    }
    public void seeDoor() {
        System.out.println("看门中....");
    }
}
class Cat extends Animal {
    public void shout() {
        System.out.println("喵喵喵喵!");
    }
}
public class TestPolym {
    public static void main(String[] args) {
        Animal a1 = new Cat(); // 向上可以自动转型
        //传的具体是哪一个类就调用哪一个类的方法。大大提高了程序的可扩展性。
        animalCry(a1);
        Animal a2 = new Dog();
        animalCry(a2);//a2为编译类型,Dog对象才是运行时类型。
         
        //编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
        // 否则通不过编译器的检查。
        Dog dog = (Dog)a2;//向下需要强制类型转换
        dog.seeDoor();
    }
 
    // 有了多态,只需要让增加的这个类继承Animal类就可以了。
    static void animalCry(Animal a) {
        a.shout();
    }
 
    /* 如果没有多态,我们这里需要写很多重载的方法。
     * 每增加一种动物,就需要重载一种动物的喊叫方法。非常麻烦。
    static void animalCry(Dog d) {
        d.shout();
    }
    static void animalCry(Cat c) {
        c.shout();
    }*/
}

执行结果:
在这里插入图片描述
示例 5-12 给大家展示了多态最为常见的一种用法,即父类引用做方法的形参,实参可以是任意的子类对象,可以通过不同的子类对象实现不同的方式。

由此,我们可以看出多态的主要优势是提高了代码的可扩展性,符合开闭原则。但是多态也有弊端,就是无法调用子类特有的功能,比如,我们不能使用父类的引用变量调用 Dog 类特有的 seeDoor()方法。

那如果我们就想使用子类特有的功能行不行呢?行!这就是我们下一章所讲的内容:对象的转型。

5.6 对象的转型(casting)

父类引用指向子类对象,我们称这个过程为向上转型,属于自动类型转换。

向上转型后的父类引用变量只能调用它编译类型的方法,不能调用它运行时类型的方法。这时,我们就需要进行类型的强制转换,我们称之为向下转型!

【示例5-13 对象的转型

public class TestCasting {
    public static void main(String[] args) {
        Object obj = new String("北京尚学堂"); // 向上可以自动转型
        // obj.charAt(0) 无法调用。编译器认为obj是Object类型而不是String类型
        /* 编写程序时,如果想调用运行时类型的方法,只能进行强制类型转换。
         * 不然通不过编译器的检查。 */
        String str = (String) obj; // 向下转型
        System.out.println(str.charAt(0)); // 位于0索引位置的字符
        System.out.println(obj == str); // true.他们俩运行时是同一个对象
    }
}

执行结果:
在这里插入图片描述

在向下转型过程中,必须将引用变量转成真实的子类类型(运行时类型)否则会出现类型转换异常 ClassCastException。

【示例 5-14】 类型转换异常

public class TestCasting2 {
    public static void main(String[] args) {
        Object obj = new String("北京尚学堂");
        //真实的子类类型是String,但是此处向下转型为StringBuffer
        StringBuffer str = (StringBuffer) obj;
        System.out.println(str.charAt(0));
    }
}

执行结果为:

在这里插入图片描述

为了避免出现这种异常,我们可以使用 5.1.2 中所学的 instanceof 运算符进行判断:

public class TestCasting3 {
    public static void main(String[] args) {
        Object obj = new String("北京尚学堂");
        if(obj instanceof String){
            String str = (String)obj;
            System.out.println(str.charAt(0));
        }else if(obj instanceof StringBuffer){
            StringBuffer str = (StringBuffer) obj;
            System.out.println(str.charAt(0));
        }
    }
}

执行结果:

在这里插入图片描述

5.7 final 关键字

final 关键字的作用

  1. 修饰变量:被他修饰的变量不可改变。一旦赋了初值,就不能被重新赋值。
final  int   MAX_SPEED = 120;
  1. 修饰方法:该方法不可被子类重写,但是可以被重载!
final  void  study(){}

3。 修饰类:修饰的累不鞥被继承。比如:Math、String等。

final   class  A {}

final 修饰变量详见 第二章示例 2-9

final 修饰方法:

在这里插入图片描述

final 修饰类:

在这里插入图片描述

5.8 抽象方法和抽象类

抽象方法

使用abstract 修饰的方法,没有方法体,只有声明。定义的是一种“规范”,就是告诉子类必须要给抽象方法提供具体的实现。

抽象类

包含抽象方法的类就是抽象类。通过abstract 方法定义规范,然后要求子类必须定义具体实现。通过抽象类,我们就可以做到严格限制子类的设计,使子类之间更加通用。

【示例5-16】抽象类和抽象方法的基本用法

//抽象类
abstract class Animal {
    abstract public void shout();  //抽象方法
}
class Dog extends Animal { 
    //子类必须实现父类的抽象方法,否则编译错误
    public void shout() {
        System.out.println("汪汪汪!");
    }
    public void seeDoor(){
        System.out.println("看门中....");
    }
}
//测试抽象类
public class TestAbstractClass {
    public static void main(String[] args) {
        Dog a = new Dog();
        a.shout();
        a.seeDoor();
    }
}

抽象类的使用要点

  1. 有抽象方法的类只能定义成抽象类
  2. 抽象类不能实例化,即不能用 new 来实例化抽象类。
  3. 抽象类可以包含属性、方法、构造函数。但是构造方法不饿能用来 new 实例,只能用来被子类调用。
  4. 抽象类只能用来继承。
  5. 抽象方法必须被子类实现。

5.9.1 接口的作用

为什么需要接口?接口和抽象类的区别?

接口就是比“抽象类”还“抽象”的抽象类,可以更加规范的对子类进行约束。全面地专业地实现了:规范和具体实现的分离。

抽象类好提供某些具体实现,接口不提供任何实现,接口中所有方法都是抽象方法。接口是完全面向规范的,规定了以批类具有的公共方法规范。

从接口的实现者角度看,接口定义了可以向外部提供的服务。

从接口的调用者角度看,接口定义了实现者能提供哪些服务。

接口是两个模块之间通信的标准,通信的规范。如果能把你要设计的模块之间的接口定义好,就相当于完成了系统的设计大纲,剩下的就是添砖加瓦的具体实现了。大家在工作以后,做系统时往往就是使用“面向接口”的思想来设计系统。

接口和实现类不是父子关系,是实现规则的关系。比如:我定义了一个接口 Runnable,Car 实现它就能在地上跑,Train实现它也能在地上跑,飞机实现它也能在地上跑。就是说,如果它是交通工具,就一定能跑,但是一定要实现 Runnable 接口。

接口的本质探讨

接口就是规范,定义的是一组规则,体现了实现世界中“如果你是。。则必须能。。”的思想。如果你是天使,则必须能飞。如果你是汽车,则必须能跑。如果你是好人,则必须能干掉坏人;如果你是坏人,则必须欺负好人。

接口的本质是契约,就像我们人间的法律一样。制定好后大家都遵守。

面向对象的精髓,是对对象的抽象,最能体现这一点的就是接口。为什么我们讨论设计模式都只针对具备了抽象能力的语言(比如C++、Java、C#等),就是因为设计模式所研究的,实际上就是如何合理的去抽象。

区别

  1. 普通类:具体实现
  2. 抽象类:具体实现、规范(抽象方法)
  3. 接口:规范!

5.9.2 如何定义和使用接口

声明格式:

[访问修饰符]  interface 接口名   [extends  父接口1,父接口2…]  {
常量定义;  
方法定义;
}

定义接口的详细说明:

  1. 访问修饰符:只能是 public 或默认。
  2. 接口名:和类名采用相同命名机制。
  3. extends:接口可以多继承。
  4. 常量:接口中的属性只能是常量,总是:public static final 修饰。不写也是。
  5. 方法:接口中的方法只能是:public abstract。省略的话,也是 public abstract。

要点

  1. 子类通过 implements 来实现接口中的规范。
  2. 接口不能创建实例,但是可用于声明引用变量类型。
  3. 一个类实现了接口,必须时间接口中的所有方法,并且这些方法只能是public 的。
  4. JDK 1.7 之前,接口中之鞥呢包含青苔常量、抽象方法,不能有普通属性、构造方法、普通方法。
  5. JDK 1.8 之后,接口中包含普通的静态方法。

【示例5-17】接口的作用

public class TestInterface {
    public static void main(String[] args) {
        Volant volant = new Angel();
        volant.fly();
        System.out.println(Volant.FLY_HIGHT);
         
        Honest honest = new GoodMan();
        honest.helpOther();
    }
}
/**飞行接口*/
interface Volant { 
    int FLY_HIGHT = 100;  // 总是:public static final类型的;
    void fly();   //总是:public abstract void fly();
}
/**善良接口*/
interface Honest { 
    void helpOther();
}
/**Angle类实现飞行接口和善良接口*/
class Angel implements Volant, Honest{
    public void fly() {
        System.out.println("我是天使,飞起来啦!");
    }
    public void helpOther() {
        System.out.println("扶老奶奶过马路!");
    }
}
class GoodMan implements Honest {
   public void helpOther() {
        System.out.println("扶老奶奶过马路!");
    }  
}
class BirdMan implements Volant {
    public void fly() {
        System.out.println("我是鸟人,正在飞!");
    }
}

执行结果:

在这里插入图片描述

5.9.3 接口的多继承

接口完全支持多继承。和类的继承类似,子接口扩展某个父接口,将会获得父接口中所定义的一切。

【示例 5-18】 接口的对继承

interface A {
    void testa();
}
interface B {
    void testb();
}
/**接口可以多继承:接口C继承接口A和B*/
interface C extends A, B {
    void testc();
}
public class Test implements C {
    public void testc() {
    }
    public void testa() {
    }
    public void testb() {
    }
}

5.9.4面向接口编程

面向接口编程时面向对象编程的一部分。

为什么需要面向接口编程?软件设计中最难处理的就是需求的复杂变化,需求的变化更多的体现在具体实现上。我们的编程如果围绕具体实现来展开就会陷入“复杂变化”的汪洋大海中,软件也就不能最终实现。我们必须围绕某种稳定的东西开展,才能以静制动,实现规范的高质量的项目。

接口就是规范,就是项目中最稳定的东东!面向接口编程可以让我们把握住真正核心的东西,使实现复杂多变的需求成为可能。

通过面向接口编程,而不是面向实现类编程,可以大大降低程序模块间的耦合性,日高整个系统的可扩展性和可维护性。

面向接口编程的概念比接口本身的概念要大得多。设计阶段相对比较困难,在你没有写实现时就要想好接口,接口一变就乱套了,所以设计要比实现难!

老鸟建议

接口语法本身非常简单,但是如何真正使用?这才是大学问。我们需要在后面的项目中反复使用,大家才能体会到。学到此处,能了解基本概念,熟悉基本语法,就是“好学生”了,请继续努力!再请工作后,闲余时间在看看上面这段话,相信你会有更深的体会。

5.10.1 内部类的概念

一般情况,我们把类定义成独立的单元。有些情况下,我们把一个类放在另一个类的内部定义,称为内部类(innerclasses)。

内部类可以使用 public、default、protected、private 以及 static 修饰。而外部顶级类(我们以前接触的类)只能使用 public 和 default 修饰。

注意

内部类只是一个编译时概念,一旦我们编译成功,就会称为完全不同的两个类。对于一个名为 Outer 的外部类和其内部定义的名为 Inner 的内部类。编译完成后会出现 Outer.class he Outer$Inner.class 两个类的字节码文件。所以内部类是相对独立的一种存在,其成员变量/方法名可以和外部类相同。

【示例5-19】 内部类介绍

/**外部类Outer*/
class Outer {
    private int age = 10;
    public void show(){
        System.out.println(age);//10
    }
    /**内部类Inner*/
    public class Inner {
        //内部类中可以声明与外部类同名的属性与方法
        private int age = 20;
        public void show(){
            System.out.println(age);//20
        }
    }
}

示例5-19 编译之后会产生两个不同的字节码文件:

在这里插入图片描述

内部类的作用

  1. 内部类提供了更好的封装。只能让外部类直接访问,不允许同一个包中的其他类直接访问。
  2. 内部类可以直接访问外部类的私有属性,内部类被当成其外部类的成员。但外部类不能访问内部类的内部属性。
  3. 接口只是解决了多重继承的部分问题,而内部类使得多重继承的解决方法变得更加完整。

内部类的使用场合

  1. 由于内部类提供了更好的封装性,并且可以很方便地访问外部类的属性。所以,在只为外部类提供服务的情况下可以优先考虑使用内部类。
  2. 使用内部类间接实现多重继承:每个内部类都能独立地继承一个类或者实现某些接口,所以无论外部类是否已经继承了某个类或者实现了某些接口,对于内部类没有任何影响。

5.10.2 内部类的分类

在Java 中内部类主要分为成员内部类(非静态内部类、静态内部类)、匿名内部类、局部内部类。

成员内部类(可以使用 private、default、protected、public 任意进行修饰。类文件:外部类$内部类.class)

非静态内部类(外部类里使用非静态内部类和平时使用其他类没什么不同)

非静态内部类必须寄存在一个外部类对象里。因此,如果有一个非静态内部类对象那么一定存在对应的外部对象。非静态内部类对象单独属于外部类的某个对象。

非静态内部类可以直接访问外部类的成员,但是外部类不能直接访问非静态内部类成员。

非静态内部类不能有静态方法、静态属性和静态初始化块。

外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例。

成员变量访问要点:

  1. 内部类里方法的局部变量:变量名。
  2. 内部类属性:this.变量名。
  3. 外部类属性:外部类名.this.变量名。

【示例5-20】 成员变量的访问要点

class Outer {
    private int age = 10;
    class Inner {
        int age = 20;
        public void show() {
            int age = 30;
            System.out.println("内部类方法里的局部变量age:" + age);// 30
            System.out.println("内部类的成员变量age:" + this.age);// 20
            System.out.println("外部类的成员变量age:" + Outer.this.age);// 10
        }
    }
}

内部类的访问:

  1. 外部类中定义内部类:
new Inner()
  1. 外部类以外的地方使用非静态内部类:
Outer.Inner  varname = new Outer().new Inner()

【示例5-21】 内部类的访问

public class TestInnerClass {
    public static void main(String[] args) {
        //先创建外部类实例,然后使用该外部类实例创建内部类实例
        Outer.Inner inner = new Outer().new Inner();
        inner.show();
        Outer outer = new Outer();
        Outer.Inner inn = outer.new Inner();
        inn.show();
    }
}

执行结果:

在这里插入图片描述

静态内部类

定义方式

static  class   ClassName {
//类体
}

使用要点

  1. 当一个静态内部类对象存在,并不一定存在对应的外部类对象。因此,静态内部类的实例方法不能直接访问外部类的实例方法。
  2. 静态内部类看做外部类的一个静态成员。因此,外部类的方法中可以通过:“静态内部类.名字” 的方式访问静态内部类的静态成员,通过 new 静态内部类() 访问静态内部类的实例。

【示例5-22】静态内部类的访问

class Outer{
    //相当于外部类的一个静态成员
    static class Inner{
    }
}
public class TestStaticInnerClass {
    public static void main(String[] args) {
        //通过 new 外部类名.内部类名() 来创建内部类对象
        Outer.Inner inner =new Outer.Inner();
    }
}

匿名内部类

适合哪种只需要使用一次的类。比如:键盘监听操作等等。

语法:

new  父类构造器(实参类表) \实现接口 () {
           //匿名内部类类体!
}

【示例5-23】 匿名内部类的使用

this.addWindowListener(new WindowAdapter(){
        @Override
        public void windowClosing(WindowEvent e) {
            System.exit(0);
        }
    }
);
this.addKeyListener(new KeyAdapter(){
        @Override
        public void keyPressed(KeyEvent e) {
            myTank.keyPressed(e);
        }      
        @Override
        public void keyReleased(KeyEvent e) {
            myTank.keyReleased(e);
        }
    }
);

注意

  1. 匿名内部类没有访问修饰符
  2. 匿名内部类没有构造方法。因为它连名字都没有那又何来构造方法呢。

局部内部类

还有一种内部类,它是定义在方法内部的,作用域只限于本方法,称为局部内部类。

局部内部类的使用主要是用来解决比较复杂的问题,想创建一个类来复制我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类。局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法中被使用,出了该方法就会失效。

【示例5-24】 方法中的内部类

public class Test2 {
    public void show() {
        //作用域仅限于该方法
        class Inner {
            public void fun() {
                System.out.println("helloworld");
            }
        }
        new Inner().fun();
    }
    public static void main(String[] args) {
        new Test2().show();
    }
}

在这里插入图片描述

5.11.1 String基础

  1. String 类又称作不可变字符序列
  2. String位于java.lang包中,Java 程序默认导入java.lang 包下的所有类。
  3. Java 字符串是就是 Unicode 字符序列,例如字符串 “Java” 就是 4个Unicode 字符 ’J’、’a’、’v’、’a’组成的。
  4. Java 没有内置的字符串类型,而是在标准Java 类库中提供了一个预定义的类 String,每个用双引号括起来的字符串都是String 类的一个实例。

【示例5-25】 String 类的实例

String e = ""  ; // 空字符串
String greeting = " Hello World ";
  1. Java 允许使用符号 “+” 把两个字符串连接起来。

【示例5-26】 字符串连接

String s1 = "Hello";
String s2 = "World! ";
String s = s1 + s2; //HelloWorld!

6、 符号 + 把两个字符串按给定的顺序连接在一起,并且是完全按照给定的形式。
7. 当 + 运算符两侧的操作数中只要有一个是字符串(String)类型,系统会自动将另一个操作数转换为字符串后再进行连接。

【示例5-27】 + 连接符

int age = 18;
String str = "age is" + age;  //str赋值为"age is 18"
//这种特性通常被用在输出语句中:
System.out.println("age  is" + age);

5.11.2 String类和常量池

在Java 的内存分析中,我们会经常听到关于 “常量池” 的描述,实际上常量池也分了以下三种:

全局字符常量池(String Pool)

全局字符串常量池中存放的内容是在类加载完成后存到 String Pool 中的,在每个 VM 中只有一份,存放的是字符串常量的引用值(在堆中生成字符串对象实例)

class 文件常量池(Class Constant Pool)

class 常量池是在编译的时候每个 class 都有的,在编译阶段,存放的是 常量(文本字符串、final 常量等)和符号引用。

运行时常量池(Runtime Constant Pool)

运行时常量池是在类加载之后,将每个class 变量池中的符号引用值转存到运行时常量池中,也就是说,每个class 都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

【示例 5-28】常量池

String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);// true
System.out.println(str2 == str4);// false
System.out.println(str4 == str5);// true

首先经过编译之后,在该类的class 变量池中存放一些顾浩引用,然后类加载之后,将class 常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的“abc”实例对象),然后将这个对象的引用存到全局 String Pool 中,也就是 String Pool 总,最后在解析阶段,要把运行时常量池中的符号引用地换成直接引用,那马就直接查询 String Pool ,保证 String Pool 里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。

回到示例5-28 的那个程序,现在就很容易解释整个程序的内存分配过程了,首先,在堆中会有一个 “abc” 实例,全局 String Pool 中存放着 “abc” 的一个引用值,然后在运行第二句的时候会生成两个实例,一个是 “def” 的实例对象,并且 String Pool 中存储一个“def”的引用值,还有一个是 new 出来的 一个 “def” 的实例对象,与上面那个是不同的实例,当在解析 str3 的时候查找 String Pool ,里面有“abc”的全局驻留字符串引用,所以str3 的引用地址与之前的那个已存在的相同,str4是在运行的时候调用 intern()方法,返回 String Pool 中 “def” 的引用值,如果没有就将 str2 的引用值添加进去,在这里,String Pool 中已经有了 “def” 的引用值,最后 str5 在解析的时候就也是指向存在于 String Pool 中的 “def” 的引用值,那么这样一分析之后,结果就容易理解了。

5.11.3 阅读 API 文档

如何下载API文档

  1. 下载地址,点击进入

http://www.oracle.com/technetwork/java/javase/documentation/jdk8-doc-downloads-2133158.html

在这里插入图片描述

  1. 下载成功后,解压下载的压缩文件,点击进入 docs/api 下的 indexl.html 即可。

在这里插入图片描述

API 文档如何阅读

在这里插入图片描述

注意

eclipse 中将鼠标放到类或方法上, 即可看到相关的注释说明;再按下 F2 即可将注释窗口固定。

在这里插入图片描述

5.11.4 String 类常用的方法

String类是我们最常使用的类。字符串类的方法我们必须非常熟悉!我们列出常用的方法,请大家熟悉。

在这里插入图片描述
【示例5-29】String类常用方法一

public class StringTest1 {
    public static void main(String[] args) {
        String s1 = "core Java";
        String s2 = "Core Java";
        System.out.println(s1.charAt(3));//提取下标为3的字符
        System.out.println(s2.length());//字符串的长度
        System.out.println(s1.equals(s2));//比较两个字符串是否相等
        System.out.println(s1.equalsIgnoreCase(s2));//比较两个字符串(忽略大小写)
        System.out.println(s1.indexOf("Java"));//字符串s1中是否包含Java
        System.out.println(s1.indexOf("apple"));//字符串s1中是否包含apple
        String s = s1.replace(' ', '&');//将s1中的空格替换成&
        System.out.println("result is :" + s);
    }
}

在这里插入图片描述

【示例5-30】 String类常用方法二

public class StringTest2 {
    public static void main(String[] args) {
        String s = "";
        String s1 = "How are you?";
        System.out.println(s1.startsWith("How"));//是否以How开头
        System.out.println(s1.endsWith("you"));//是否以you结尾
        s = s1.substring(4);//提取子字符串:从下标为4的开始到字符串结尾为止
        System.out.println(s);
        s = s1.substring(4, 7);//提取子字符串:下标[4, 7) 不包括7
        System.out.println(s);
        s = s1.toLowerCase();//转小写
        System.out.println(s);
        s = s1.toUpperCase();//转大写
        System.out.println(s);
        String s2 = "  How old are you!! ";
        s = s2.trim();//去除字符串首尾的空格。注意:中间的空格不能去除
        System.out.println(s);
        System.out.println(s2);//因为String是不可变字符串,所以s2不变
    }
}

在这里插入图片描述

5.11.5 字符串相等的判断

  1. equals 方法用来检测两个字符串内容是否相等。如果字符串 s 和 t 内容相等,则 s.equals(t)返回true,否则返回 false。
  2. 要测试两个字符串除了大小写区别外是否相等的,需要使用 equalsIgnoreCase 方法。
  3. 判断字符串是否相等不要使用 “==”。

【示例5-31】 忽略大小写的字符串比较

"Hello".equalsIgnoreCase("hellO");//true

【示例5-32】字符串的比较 “==” 与 equals()方法

public class TestStringEquals {
    public static void main(String[] args) {
        String g1 = "北京尚学堂";
        String g2 = "北京尚学堂";
        String g3 = new String("北京尚学堂");
        System.out.println(g1 == g2); // true  指向同样的字符串常量对象
        System.out.println(g1 == g3); // false  g3是新创建的对象
        System.out.println(g1.equals(g3)); // true  g1和g3里面的字符串内容是一样的
    }
}

执行结果:
在这里插入图片描述

示例5-32的内存分析:

在这里插入图片描述

5.12.1 开闭原则

开闭原则(Open-Closed Principe)就是让设计的系统对罗占开放,对修改封闭

  • 对扩展开放:
    就是指,应对需求变化要灵活。要增加新功能时,不需要修改已有的代码,增加代码即可。

  • 对修改关闭:
    就是指,核心部分经过精心设计后,不在因为需求变化而改变。
    在实际开发中,我们无法完全做到,但应尽量遵守开闭原则。

5.12.2 模板方法模式和回调机制

模板方法模式很常用,其目的是在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。在标准的模板方法模式实现中,主要是使用继承的方式,来让父类在运行期间可以调用到子类的方法。详见抽象类部分示例。

其实在Java 开发中,还有另外一个方法可以实现同样的功能,那就是 Java 回调技术。回调是一种双向的调用模式,也就是说,被调用的接口被调用时也会调用对方的接口,简单点说明就是:A类汇总调用B类总的C方法,然后 B类中的C方法 中反过来调用 A类中的 D方法,那么D这个方法就叫回调方法。

回调的具体过程如下:

  1. Class A 实现接口 CallBack —— 背景1
  2. Class A 中包含 Class B 的引用 —— 背景2
  3. Class B 有一个参数为 CallBack 的方法 C——背景3
  4. 前三条是我们的准备条件,接下来A 的对象调用B 的方法C
  5. 然后 Class B 就可以在 C 方法中调用 A 的 方法 D

这样说大家可能好事不太理解,下面我们根据示例5-33来说明回调机制。

【示例5-33】 回调机制示例

/** 
 * 回调接口  
 */
interface CallBack {  
    /** 
     * 小高知道答案后告诉小刘时需要调用的方法,即回调方法
     * @param result 是问题的答案 
     */  
    public void answer(String result);  
}
/*
/** 
 * 小刘类:实现了回调接口CallBack(背景一) 
 */  
class Liu implements CallBack {  
    /** 
     * 包含小高对象的引用 (背景二) 
     */  
    private Gao gao;   
  
    public Liu(Gao gao){  
        this.gao = gao;  
    }  
       
    /** 
     * 小刘通过这个方法去问小高 
     * @param question  小刘问的问题“学习Java选哪家机构呢?” 
     */  
    public void askQuestion(String question){  
        //小刘问小高问题
        gao.execute(Liu.this, question);          
    }  
    /** 
     * 小高知道答案后调用此方法告诉小刘
     */  
    @Override
    public void answer(String result) {
        System.out.println("小高告诉小刘的答案是:" + result);        
    }  
} 
/** 
 * 小高类 
 */  
class Gao {
     /** 
     * 相当于class B有一个参数为CallBack的方法C(背景三) 
     */  
    public void execute(CallBack callBack, String question){  
        System.out.println("小刘问的问题是:" + question);  
        //模拟小高挂点后先办自己的事情花了很长时间  
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //小高办完自己的事情后想到了答案 
        String result = "学Java当然去北京尚学堂";  
        //小高打电话把答案告诉小刘,相当于class B 反过来调用class A 的D方法 
        callBack.answer(result);         
    }  
}
 
public class Test {
    public static void main(String[] args) {
        Gao  gao= new Gao();  
        Liu liu = new Liu(gao);  
        //小刘问问题
        liu.askQuestion("学习Java选哪家机构呢?"); 
    } 
}

5.12.3 组合模式

组合模式是将对象组合成属性结构以表示“部分-整体”的层次结构。组合模式使用得用户对单对象和组合对象的使用具有一致性。

【示例5-34】对象的组合

class Cpu {
    public void run() {
        System.out.println("quickly.........");
    }
}
class MainBoard {
    public void connect() {
        System.out.println("connect...........");
    }
}
class Memory {
    public void store() {
        System.out.println("store........");
    }
}
public class Computer {
    Cpu cpu;
    Memory memory;
    MainBoard mainBoard;
 
    public void work() {
        cpu.run();
        memory.store();
        mainBoard.connect();
    }
     
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.cpu = new Cpu();
        computer.mainBoard = new MainBoard();
        computer.memory = new Memory();
        computer.work();
    }
}

在这里插入图片描述

总结

  • 高级语言语言可分为:面向过程和面向对象两大类
  1. 面向过程与面向对象都是解决问题的思维方式,都是代码组织的方式
  2. 解决简单问题可以使用面向过程。
  3. 解决复杂问题:宏观上使用面向对象把握,微观处理上仍然是面向过程。
  • 对象和类的关系是特使到一般,具体到抽象的关系。
  • 栈内存
  1. 每个线程私有,不能实现线程间的共享!
  2. 局部变量放置于栈中。
  3. 栈是由系统自动分配,速度快!栈是一个连续的内存空间!
  • 堆内存
  1. 放置new 出来的对象
  2. 堆是一个不连续的内存空间,分配灵活,速度慢!
  • 方法区
  1. 被所有线程共享!
  2. 用来存放程序中永远不变或唯一的内容(类代码信息、静态变量、字符串常量)。
  • 属性用于定义该类或该类对象包含的数据或者说静态属性。属性作用范围是整个类体。

Java 使用默认的值对其初始化。

– 构造器又叫构造方法(constructor),用于构造该类的实例。Java 通过 new 关键字来调用构造方法,从而返回该类的实例,是一种特殊的方法。

  • 垃圾回收机制
  1. 程序员无权调用垃圾回收器。
  2. 程序员可以通过 System.gc()通知垃圾回收器(Garbage Colllection,简称GC)运行,但是Java 规范并不鞥保证立刻运行。
  3. finalize方法,是Java 提供给程序员用来释放对象或资源的方法,但是尽量少用。
  • 方法的重载是指一个类中可以定义有相同的名字,但参数不同的多个方法。调用时,会根据不同的参数表选择对应的方法。

  • this 关键字的作用

  1. 让类中的一个方法,访问该类的另一个方法或属性。
  2. 使用this 关键字调用重载构造方法,可以避免相同的初始化代码,只能在构造方法中用,并且必须位于构造方法的第一句。
  • static 关键字
  1. 在类中,用 static 声明的成员变量为静态成员变量,也称为类变量。
  2. 用 static 声音的方法为静态方法。
  3. 可以通过对象引用或类名(不需要实例化)访问静态成员。
  • import 的作用
  1. 铜鼓哦import 可以导入其他包下面的类,从而可以在蓓蕾中直接通过类名来调用。
  • super 关键字的作用
  1. super 是直接父类对象的引用。可以通过 super 来访问父类中被子类覆盖的方法或属性。
  • 面向对象的三大特征:继承、封装、多态。
  • Object 类是所有Java类的根基类。
  • 访问权限控制符:范围由小到大分别是 private、default、protected、public
  • 引用变量名 instanceof 类名 ,来判断该引用类型变量所“指向”的随想是否属于该类或该类的子类。
  • final 关键字可以修饰变量、修饰方法、修饰类。
  • 抽象类是一种模板模式。抽象类为所有子类提供了一个通用模板,子类可以在在这个模板基础上进行扩展,使用 abstract 修饰。
  • 使用 abstract 修饰的方法为抽象方法必须被子类实现,除非子类也是抽象类。
  • 使用 interface 声明接口
  1. 从接口的实现者角度看,接口定义了可以向外部提供的服务。
  2. 从接口的调用者角度看,接口定义了实现者能提供哪些服务。
  • 内部类分为 成员内部类、匿名内部类和局部内部类。
  • String 位于 java.lang 包中,Java 程序默认导入 java.lang包。
  • 字符串的比较 “==” 与 equals()方法的区别。

猜你喜欢

转载自blog.csdn.net/weixin_44626569/article/details/89139338