第5章 - 面向对象(上)

面向对象(上)

1. 类和对象

1.1 定义类

[修饰符] class 类名 {
	1. 成员变量
	2. 构造器
	3. 方法
	4. 内部类
	5. 初始化块
}
  • 修饰符:可以是 public、final、abstract,或者省略;

  • 类名:合法的标识符,一般首字母大写。

1.2 定义成员变量

[修饰符] 类型 变量名 [= 默认值];
  • 修饰符:可以是 public | protected |private、static、final,或他们的组合,或者省略;
  • 变量名:合法的标识符,一般采用小驼峰命名法,多使用名词。

1.3 定义方法

[修饰符] 返回值类型 方法名(形参列表) {
	// 语句
}
  • 修饰符:public | protected | private、static、final | abstract,或他们的组合,或者省略;
  • 返回值类型:Java 语言允许的任何类型,没有返回值时,需要写 void;
  • 方法名:合法的标识符,一般采用小驼峰命名法;
  • 形参列表:可以有零到多个形参,多个形参之间用英文 , 分隔开。

1.4 定义构造器

[修饰符] 构造器名(形参列表) {
	// 语句
}
  • 修饰符:public | protected | private 中的一个, 或者省略不写;
  • 构造器名:必须和类名相同;
  • 形参列表:可以为零或多个形参。

1.5 定义初始化块

[修饰符] {
	// 代码
}
  • 修饰符:static 或者不写。

1.6 对象的产生和使用

定义类:

public class Person {
    public String name;
    public int age;
    public void say(String content) {
        System.out.println(content);
    }
}

创建对象:

Person p;
p = new Person();

简写方法:

Person p = new Person();

使用对象:

p.name = "东方未明";
p.say("我要成为天下第一!")

1.7 this

this 关键字总是指向调用该方法的对象:

  • 构造器中引用该构造器正在初始化的对象
  • 在方法中引用调用该方法的对象
public class Person {
    // 定义类访问权限的成员变量
    private String name;
    private int age;
    
    // 定义不带参数的构造器
    public Person() {}
    
    // 定义带两个参数的构造器
    public Person(String name, int age) {
        // this 在构造器中引用正在初始化的对象
        this.name = name;
        this.age = age;
    }
    
    // 定义修改名字的方法
    public void setName(String name) {
        // this 在方法中调用对象
        this.name = name;
    }
    
    // 定义获取名字的方法
    public String getName() {
        return this.name;
    }
    
    // 定义一个简单的方法
    public void sayHello() {
        System.out.println("天上一声雷,惊醒世间谁!");
    }
    
    // static 修饰的方法中,不能使用 this 引用
    public static void main(String[] args) {
        // 不可以直接引用
        // this.sayHello();
        // 只能通过新建对象的方式引用
        new Persion().sayHello();
        
    }
    
}

2. 方法

方法是类或对象的行为特征的抽象,从功能上看,类似于传统结构化程序设计里的函数。 Java 里的方法不能独立存在,所有方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。

2.1 方法的所属性

有 static 修饰符的方法属于该类本身,即可以被类调用,也可以被对象调用;没有 static 修饰符的方法,只能被对象调用。

public class Test {
    public static void foo() {
        System.out.println("道可道,非常道;名可名,非常名。");
    }
    
    public void bar() {
        System.out.println("无,名天地之始;有,名万物之母。");
    }
    
    public static void main(String[] args) {
        // 有 static 修饰符的方法属于该类本身,即可以被类调用,也可以被对象调用
        Test.foo();
        new Test().foo();
        
        // 没有 static 修饰符的方法,只能被对象调用
        new Test().bar();
    }
}

2.2 形参个数可变方法

public class Varargs {
    // 参数个数可变方法,一个方法中有且只能存在一个,且必须放在最后
    public static void test(int a, String... books) {
        for (String book : books) {
            System.out.println(book);
        }
        System.out.println(a);
    }

    public static void main(String[] args) {
        // 可以有两种方式传参,分别是通过数组方式和直接赋值
        String[] books1 = {"Java从入门到放弃", "程序员的头部保养", "数据库从删库到跑路"};
        test(5, books1);
        test(6, "道德经", "南华经");
    }

}

2.3 递归方法

一个方法体内调用它自身,被称为方法递归。递归方法用处很多,比如需要遍历目录下所有文件,但目录深度未知,就可以使用递归的方法。

// 计算阶乘
public class Factorial {
    public static int fn(int n) {
        if (n == 0 || n == 1) {
            return 1;
        } else {
            return n * fn(n - 1);
        }
    }
    
    public static void main(String[] args) {
        System.out.println(fn(5));  // 120
    }
}

2.4 方法重载

Java 允许同一个类里定义多个同名方法,只要形参列表不同就行。如果同一个类中出现了这个现象,则称为方法的重载。

public void test() {
    //
}

public void test(String msg) {
    //
}

public void test(String... msg) {
    //
}

// 系统会正常区分调用的哪个方法
xx.test();
xx.test("Hello");

// 这里会调用第三个方法
xx.test("Hello", "Welcome", "to", "China");
xx.test(new String[] {"aaa"});

// 如果未定义无参数的方法,当调用 xx.test() 时,也会调用第三个方法

3. 成员变量和局部变量

  • 成员变量
    • 类变量(以 static 修饰)
    • 实例变量(不以 static 修饰)
  • 局部变量
    • 形参
    • 方法局部变量
    • 代码块局部变量

变量的使用规则

合理的使用成员变量;

因尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存中停留的时间就越短,程序运行性能就越好。

4. 隐藏和封装

4.1 理解封装

封装的好处:

  • 隐藏类的实现细节;
  • 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问;
  • 可进行数据检查,从而有利于保证对象信息的完整性;
  • 便于修改,提高代码的可维护性;

需要考虑的两个方面:

  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问;
  • 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

4.2 访问控制符

访问控制级别privatedefaultprotectedpublic
同一个类中
同一个包中
子类中
全局范围中

模块设计的追求:

  • 高内聚:尽可能把模块的内部数据、功能实现细节隐藏在模块内部独立完成,不允许外部直接干预;
  • 低耦合:仅暴露少量的方法给外部使用。

4.3 package、import 和 import static

Java 允许将一组功能相关的类放在同一个 package(包)下,从而组成逻辑上的类库单元。

如果希望把一个类放在指定包结构下,需要在 java 源程序的第一个非注释行放置如下格式代码:

package pakeageName;

public class Hello {
    // 代码
}

引用其他指定包下的类:

// 引用 java.util 包下 Arrays 类
import java.util.Arrays;

// 引用 java.sql 包下所有类
import java.sql.*;

如果引入的类存在重名,在使用时需要加上类全名:

java.sql.Data d = new java.sql.Data();

导入类变量和方法:

// 导入 java.lang.System 类的变量和方法
import static java.lang.System.*;

public class Test() {
    public static void main(String[] args) {
        out.println("Hello");  // 不需要前面加 System. 了
    }
}

4.4 Java 常用包

  • java.lang:包含 Java 语言的核心类,String、Math、System、Thread 类等,使用这个包下的类无须使用 import 导入,系统会自动导入这个包下的所有类;
  • java.util:包含 Java 的大量工具类 / 接口和集合框架类 / 接口,Arrays、List、Set 等;
  • java.net:包含一些 Java 网络编程相关的类、接口;
  • java.io:包含一些 Java 输入 / 输出编程相关的类、接口;
  • java.text:包含一些 Java 格式化相关的类;
  • java.sql:包含 Java 进行 JDBC 数据库编程的相关类 / 接口;
  • java.awt:包含了抽象窗口工具集(Abstract Window Toolkits)的相关类 / 接口,这些类主要用于构建图形用户界面(GUI)程序;
  • java.swing:包含了 Swing 图形用户界面编程的相关类 / 接口,这些列可用于构建平台无关的 GUI 程序。

5. 构造器

public class ConstructorOverLoad {
    public String name;
    public String color;
    public double weight;
    
    // 无参数构造器
    public ConstructorOverLoad() {}
    
    // 两个参数的构造器
    public ConstructorOverLoad(String name, String color) {
        this.name = name;
        this.color = color;
    }
    
    // 三个参数的构造器
    public ConstructorOverLoad(String name, String color, double weight) { 
        // 通过 this 调用另一个重载的构造器的初始化代码
        this(name, color);
        this.weight = weight;
    }
    
    public static void main(String[] args) {
        new ConstructorOverLoad("Apple", "red", 4.5);
    }

}

6. 类的继承

修饰符 class SubClass extends SuperClass {
    // 代码
}

6.1 继承

// 定义父类
public class Fruit {
    public double weight;
    public void info() {
        System.out.println("我是一个水果,重:" + weight + "g!");
    }
}
// 定义子类
public class Apple extends Fruit {
    public static void main(String[] args) {
        Apple a = new Apple();
        a.weight = 56;
        a.info();  // 我是一个水果,重:56.0g!
    }
}

6.2 重写父类的方法

public class Apple extends Fruit {
    // 重写父类的方法,也叫覆盖
    public void info() {
        System.out.println("我是一个苹果,重:" + weight + "g!");
    }
    
    public static void main(String[] args) {
        Apple a = new Apple();
        a.weight = 56;
        a.info();  // 我是一个苹果,重:56.0g!
    }
}

6.3 super 限定

如果需要在子类方法中调用父类被覆盖的实例方法,则可以使用 super 限定来调用。

public class Apple extends Fruit {
    // 重写父类的方法,也叫覆盖
    public void info() {
        System.out.println("我是一个苹果,重:" + weight + "g!");
    }
    
    // 调用被覆盖的父类方法
    public void callOverridedMethods() {
        super.info();
    }
    
    public static void main(String[] args) {
        Apple a = new Apple();
        a.weight = 56;
        a.info();  // 我是一个苹果,重:56.0g!
        a.callOverridedMethods();  // 我是一个水果,重:56.0g!
    }
}

6.4 调用父类构造器

// 定义一个生物类
class Creature {
    public Creature() {
        System.out.println("Creature 无参数的构造器");
    }
}

// 定义一个动物类
class Animal extends Creature {
    public Animal(String name) {
        System.out.println("Animal 一个参数的构造器:" + name);
    }
    
    public Animal(String name, int age) {
        // 使用 this 调用一个重载的构造器
        this(name);
        System.out.println("Animal 两个参数的构造器:" + name + "、" + age);
    }
}

// 定义一个狼类
public class Wolf extends Animal {
    // 狼的构造器
    public Wolf() {
        super("灰太狼", 3);
        System.out.println("Wolf 无参数的构造器");
    }

    public static void main(String[] args) {
        new Wolf();
    }
}

运行上面的代码,得到结果:

Creature 无参数的构造器
Animal 一个参数的构造器:灰太狼
Animal 两个参数的构造器:灰太狼、3
Wolf 无参数的构造器

从上面运行过程来看,创建任何对象总是从该类所在继承树最顶层类的构造器开始执行的,然后依次向下执行,最后才执行本类的构造器。

7. 多态

Java 引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定,如果编译时类型和运行时类型不一致,就可能出现所谓的多态。

7.1 多态性

class BaseClass {
    public int book = 6;
    public void base() {
        System.out.println("父类的普通方法");
    }
    public void test() {
        System.out.println("父类的被覆盖方法");
    }
}

public class SubClass extends BaseClass {
    public String book = "西游记";
    public void test() {
        System.out.println("子类的覆盖父类的方法");
    }
    
    public void sub() {
        System.out.println("子类的普通方法");
    }

    public static void main(String[] args) {
        BaseClass bc = new BaseClass();
        System.out.println(bc.book);  // 6
        bc.base();  // 父类的普通方法
        bc.test();  // 父类的被覆盖方法
        
        SubClass sc = new SubClass();
        System.out.println(sc.book);  // 西游记
        sc.base();  // 父类的普通方法
        sc.test();  // 子类的覆盖父类的方法
        sc.sub();  // 子类的普通方法
        
        BaseClass ploymophicBc = new SubClass();
        System.out.println(ploymophicBc.book);  // 6
        ploymophicBc.base();  // 父类的普通方法
        ploymophicBc.test();  // 子类的覆盖父类的方法
        // ploymophicBc.sub();  // BaseClass 类没有 sub() 方法
    }
}

当把一个子类对象直接赋给父类引用变量时,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量,调用同一个方法时呈现出多种不同的行为特征,这就是多态。

与方法不同的是,对象的实例变量则不具备多态性。

7.2 引用变量的强制类型转换

编写 Java 程序时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法。如果需要让这个引用变量调用它运行时类型的方法,则必须把它强制类型转换成运行时类型。

强制类型转换注意点:

  • 基本类型之间的转换只能在数值类型之间进行(整数型、字符型、浮点型)
  • 引用类型之间的转换只能在具有继承关系的两个类型之间进行。如果试图把一个父类实例转换成子类类型,则这个对象必须实际上是子类实例才行(即编译时类型为父类类型,而运行时类型是子类类型)
public class ConversionTest {
    public static void main(String[] args) {
        double d = 13.14;
        long l = (long) d;
        System.out.println(l);  // 13
        
        int i = 5;
        // boolean b = (boolean) i;  // 不可转换的类型
        
        Object obj = "Hello";
        String objStr = (String) obj;
        System.out.println(obj);  // Hello
        System.out.println(objStr);  // Hello
        
        Object objPri = Integer.valueOf(5);
        int num = (int) objPri;
        System.out.println(objPri);  // 5
        System.out.println(num);  // 5
        
        // String str = (String) objPri;  // 不能将 Integer 转换为 String 类
    }
}

当把子类对象赋给父类引用变量时,被称为向上转型,这种转型总是可以成功的。但把一个父类对象赋给子类引用变量时,就需要进行强制类型转,而且还可能在运行时产生 ClassCastException 异常,使用 instanceof 运算符可以让强制类型转换更安全。

// 上面代码可进行改写
if (objPri instanceof String) {
    String str = (String) objPri;
}

7.3 instanceof 运算符

instanceof 运算符的前一个操作数,通常是一个引用类变量,后一个操作数通常是一个类(或是接口),它用于判断前面的对象是否是后面的类,或者是其子类、实现类的实例。如果是,则返回 true,否则返回 false。

在使用 instanceof 运算符时需要注意:instanceof 运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。

public class InstanceofTest {
    public static void main(String[] args) {
        Object hello = "Hello";
        System.out.println(hello instanceof Object);  // true
        System.out.println(hello instanceof String);  // true
        System.out.println(hello instanceof Math);  // false
        System.out.println(hello instanceof Comparable);  // true String 实现了 Comparable 接口
         
        String str = "Hello";
        // System.out.println(str instanceof Math);  // String 和 Math 不兼容
    }
}

8. 继承和组合

继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。

8.1 使用继承的注意点

为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则:

  • 尽量隐藏父类内部数据,比如设置成 private 访问类型;
  • 不要让子类可以随意访问、修改父类的方法。
    • 仅为辅助其他的工具方法,应该使用 private 修饰符;
    • 需要被外界访问,但不希望子类重写的方法,可以使用 public final 修饰符;
    • 希望被子类重写,但不希望被其他类自由访问的,用 protected 修饰符;
  • 尽量不要在父类构造器中调用将要被子类重写的方法。

何时适合从父类派生新的子类:

  • 子类需要额外增加成员变量,而不仅仅是变量值的改变;
  • 子类需要增加自己独有的行为方式(包括增加新的方法和重新父类的方法)

8.2 利用组合实现复用

// 定义了一个简单的胳膊类
class Arm {
    private void grasp() {
        System.out.println("抓握");
    }
    
    public void baseArm() {
        grasp();
    }
}

// 定义了一个简单的腿类
class Leg {
    private void walk() {
        System.out.println("走路");
    }
    
    public void baseLeg() {
        walk();
    }
}

// 定义了一个机器人类
class Robot {
    // 腿和胳膊是机器人的组成部分
    private Leg leg;
    private Arm arm;
    
    // 构造器
    public Robot(Arm arm, Leg leg) {
        this.arm = arm;
        this.leg = leg;
    }
    
    // 重新定义一个 baseLeg() 方法,增加了功能
    public void baseLeg() {
        leg.baseLeg();
        System.out.println("滚动");
    }
    
    // 复用 baseArm() 方法
    public void baseArm() {
        arm.baseArm();
    }
    
    // 机器人独特的方法
    public void variant() {
        System.out.println("变形");
    }
}

public class CompositeTest {
    public static void main(String[] args) {
        // 先创建必要的胳膊、腿
        Leg leg = new Leg();
        Arm arm = new Arm();
        // 然后组合成机器人
        Robot r = new Robot(arm, leg);
        
        r.baseLeg();  // 走路,滚动
        r.baseArm();  // 抓握
        r.variant();  // 变形
    }
}

何时用继承,何时用组合

  • 继承是对已有的类做一番改造,以此获得一个特殊的版本。比如:(is-a关系)动物 ➡ 狼。
  • 组合是将两个具有明确的整体、部分关系,进行组合复用的方式。比如:(has-a关系)胳膊 ➡ 人。

9. 初始化块

一个类里可以有多个初始化块,相同类型初始化块按先后定义顺序执行。

初始化块只能使用 static 修饰符,使用 static 修饰的称为类初始化块,否则为实例初始化块。

class Root {
    static {
        System.out.println("Root的类初始化块");
    }
    
    {
        System.out.println("Root的实例初始化块");
    }

    public Root() {
        System.out.println("Root的无参数的构造器");
    }
}

class Mid extends Root {
    static {
        System.out.println("Mid的类初始化块");
    }
    
    {
        System.out.println("Mid的实例初始化块");
    }

    public Mid() {
        System.out.println("Mid的无参数的构造器");
    }

    public Mid(String msg) {
        // 通过this调用同一类中重载的构造器
        this();
        System.out.println("Mid的带参数构造器,其参数值:" + msg);
    }
}

class Leaf extends Mid {
    static {
        System.out.println("Leaf的类初始化块");
    }
    
    {
        System.out.println("Leaf的实例初始化块");
    }

    public Leaf() {
        // 通过super调用父类中有一个字符串参数的构造器
        super("Java");
        System.out.println("执行Leaf的构造器");
    }
}

public class Test {
    public static void main(String[] args) {
        new Leaf();
        new Leaf();
    }
}

执行结果为:

Root的类初始化块
Mid的类初始化块
Leaf的类初始化块
Root的实例初始化块
Root的无参数的构造器
Mid的实例初始化块
Mid的无参数的构造器
Mid的带参数构造器,其参数值:Java
Leaf的实例初始化块
执行Leaf的构造器
Root的实例初始化块
Root的无参数的构造器
Mid的实例初始化块
Mid的无参数的构造器
Mid的带参数构造器,其参数值:Java
Leaf的实例初始化块
执行Leaf的构造器

在这里插入图片描述

Java 系统加载并初始化某个类时,总是保证该类的所有父类(包括直接父类和间接父类)全部加载并初始化。类初始化成功后,会在虚拟机里一直存在,不需要再次初始化。

10. 练习

  • 编写一个学生类,提供 name、age、gender、phone、address、email 成员变量,且为每个成员变量提供 setter、getter 方法。为学生类提供默认的构造器和带有所有成员变量的构造器。为学生类提供方法,用于描绘吃、喝、玩、睡等行为。
  • 利用第 1 题定义的 Student 类,定义一个 Student[] 数组保存多个 Student 对象作为通讯录数据。程序可通过 name、email、address 查询,如果找不到数据,则进行友好提示。
  • 定义普通人、老师、班主任、学生、学校这些类,提供适当的成员变量、方法用于描述其内部数据和行为特征,并提供主类使之运行。要求有良好的封装性,将不同类放在不同的包下面,增加文档注释,生成 API 文档。
  • 改写第 1 题的程序,利用组合来实现类复用。
  • 定义交通工具、汽车、火车、飞机这些类,注意它们的继承关系,为这些类提供超过 3 个不同的构造器,并通过实例初始化块提取构造器中的通用代码。
  • 将学到的知识和遇到的问题,整理成笔记,记录下来
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值