面向对象(上)
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 访问控制符
访问控制级别 | private | default | protected | public |
---|---|---|---|---|
同一个类中 | ✔ | ✔ | ✔ | ✔ |
同一个包中 | ✔ | ✔ | ✔ | |
子类中 | ✔ | ✔ | ||
全局范围中 | ✔ |
模块设计的追求:
- 高内聚:尽可能把模块的内部数据、功能实现细节隐藏在模块内部独立完成,不允许外部直接干预;
- 低耦合:仅暴露少量的方法给外部使用。
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 个不同的构造器,并通过实例初始化块提取构造器中的通用代码。
- 将学到的知识和遇到的问题,整理成笔记,记录下来