文章目录
4.1 面向对象程序设计概述(OOP)
-
概述:面向对象程序设计(简称 OOP) 是当今主流的程序设计范型, 它已经取代了 20 世纪 70年代的“ 结构化” 过程化程序设计开发技术。面向对象的程序是由对象组成的, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分。在 OOP 中, 不必关心对象的具体实现,只要能够满足用户的需求即可。
-
面向对象的显著特点:抽象性、封装性、继承性、多态性。
4.2 类(Class)
4.2.1 基本概念
-
概述:类( class) 是构造对象的模板或蓝图。由类构造(construct) 对象的过程称为创建类的实例 (instance )。
-
将数据属性和==对数据的操作 (行为或方法)==封装在一起,作为一个相互依存,不可分割的整体——类。
-
类中的大多数数据只能为本类的行为使用,类会提供公开的方法与外界进行通讯。
4.2.3 类之间的关系
- 在类之间,最常见的关系有:依赖(“use-a”)、聚合(“has-a”)、继承(“is-a”)
- 依赖(“use-a”):是一种最明显的、 最常见的关系。例如,Order类使用 Account 类是因为 Order 对象需要访问 Account 对象查看信用状态。因此, 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。
- 聚合(“has-a”):一个Order 对象包含一些 Item 对象。聚合关系意味着类 A 的对象包含类 B 的对象。
- 继承(“is-a”):是一种用于表示特殊与一般关系的。例如,RushOrder类由 Order 类继承而来。
4.2.4 类设计技巧
- 一定要保证数据私有,这是最重要的;绝对不要破坏封装性。
- 一定要对数据初始化,具体的初始化方式可以是提供默认值, 也可以是在所有构造器中设置默认值。
- 不要在类中使用过多的基本类型,就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。
- 不是所有的域都需要独立的域访问器和域更改器。
- 将职责过多的类进行分解。
- 类名和方法名要能够体现它们的职责。
- 优先使用不可变的类。
4.3 对象(Object)
4.3.1 基本概念
-
对象是具体的,实际的,代表一个具体事物, 即是类的实例。
-
对象的三个主要特性:
- 对象的行为(behavior)—可以对对象施加哪些操作,或可以对对象施加哪些方法?
- 对象的状态(state )—当施加那些方法时,对象如何响应?
- 对象标识(identity )—如何辨别具有相同行为与状态的不同对象?
-
对象在内存中的存在形式
-
Java 虚拟机在创建一个对象时都包含以下步骤:
- 给对象分配内存。
- 将对象的实例变量自动初始化为其变量类型的默认值。
- 初始化对象,给实例变量赋予正确的初始值。
4.3.2 封装(Encapsulation)
- 概念:封装( encapsulation , 有时称为数据隐藏) 是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。封装给对象赋予了“ 黑盒” 特征, 这是提高重用性和可靠性的关键。 这意味着一个类可以全面地改变存储数据的方式,只要仍旧使用同样的方法操作数据, 其他对象就不会知道或介意所发生的变化。
- 封装的意义
- 将对象的属性和行为结合在一起,形成一个不可分割的独立单位。
- 实现==“信息隐蔽”,尽可能隐藏对象的内部细节,对外界形成一个边界,只保留有限的公开方法==与外界进行联系。
- 可以对数据的获取或更改进行验证,保证安全合理。
4.3 成员方法(Method)
4.3.1 方法的概述
- 方法是具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集。
- 方法必须先创建再使用,该过程称为方法的定义。
- 方法创建后并不是直接运行的,需要手动调用后才执行,该过程称为方法的调用。
4.3.2 方法的注意事项
- 方法不能嵌套定义。
- void表示无返回值,可以省略return,也可以单独书写return后面不加东西。
- 凭经验可知, 如果需要返回一个可变数据域的拷贝,就应该使用 clone。
4.3.3 方法的参数传递
- 基本数据类型:形式参数改变,不影响实际参数的值。
- 引用数据类型:形式参数改变,影响实际参数的值。
4.3.4 方法重载(OverLoad)
-
概述:在同一个类中定义的多个方法之间的关系,满足下列多个条件的方法相互构成重载。
- 方法名称相同。
- 方法参数个数不同,或参数类型不同。
- 与返回值无关。
4.3.4.1 方法重载练习
-
计算器。
public class OverLoad01 { public static void main(String[] args) { System.out.println(calculate(1, 2)); // 3 System.out.println(calculate(1, 2.0)); // 3.0 System.out.println(calculate(1.0, 2)); // 3.0 System.out.println(calculate(1, 2, 3)); // 6.0 } public static int calculate(int n1, int n2) { return n1 + n2; } public static double calculate(int n1, double n2) { return n1 + n2; } public static double calculate(double n1, int n2) { return n1 + n2; } public static double calculate(int n1, int n2, int n3) { return n1 + n2 + n3; } }
4.3.5 方法递归调用(Recursion)
-
概述:简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂问题,同时可以让代码变得简洁。
-
示例:
4.3.5.1 递归练习
-
斐波那契数列:第一项、第二项为1、后面的项为前面两项相加。
public class Fibonacci { public static void main(String[] args) { Scanner myScanner = new Scanner(System.in); System.out.print("请输入数字:"); int n = myScanner.nextInt(); int res = fibonacciTest(n); System.out.println("第" + n + "个数是" + res); } public static int fibonacciTest(int n) { if (n == 1 || n == 2) { return 1; } else { return fibonacciTest(n - 1) + fibonacciTest(n - 2); } } }
-
猴子吃桃,猴子一天吃掉当前一半桃子后,再吃掉一个桃子,第十天剩下最后一个桃子。
public class Monkey { public static void main(String[] args) { Scanner myScanner = new Scanner(System.in); System.out.print("请输入1--10的天数:"); int day = myScanner.nextInt(); int peaches = peach(day); if (peaches != -1) { System.out.println("第" + day + "天一共有" + peaches + "个桃子"); } } public static int peach(int day) { if (day == 10) { return 1; } else if (day >= 1 && day <= 9) { return (peach(day + 1) + 1) * 2; } else { System.out.println("数据错误,请输入1--10的天数!"); return -1; } } }
-
老鼠出迷宫。
public class Maze { public static void main(String[] args) { // 0表示可以走、1表示不能走、2表示走过、3表示走过了,发现是死路、当map[6][5]为2时成功走出迷宫 int[][] map = new int[8][7]; // 最上面与最下面两行设置为1 for (int i = 0; i < 7; i++) { map[0][i] = 1; map[7][i] = 1; } // 最左边和最右边两行设置为1 for (int i = 0; i < 8; i++) { map[i][0] = 1; map[i][6] = 1; } map[3][1] = 1; map[3][2] = 1; map[2][2] = 1; // 打印初始二维数组 System.out.println("初始二维数组如下:"); for (int i = 0; i < map.length; i++) { for (int j = 0; j < map[i].length; j++) { System.out.print(map[i][j] + "\t"); } System.out.println(); } findWay(map, 1, 1); System.out.println("----------分隔符-----------"); // 方法1(下、右、上、左) System.out.println("方法1走过的二维数组如下:"); for (int i = 0; i < map.length; i++) { for (int j = 0; j < map[i].length; j++) { System.out.print(map[i][j] + "\t"); } System.out.println(); } } // 方法1(下、右、上、左) public static boolean findWay(int[][] map, int i, int j) { // i、j分别表示行、列 if (map[6][5] == 2) { return true; } else { if (map[i][j] == 0) { map[i][j] = 2; // 假定当前的位置为走过,且走得通 if (findWay(map, i + 1, j)) { // 往下走 return true; } else if (findWay(map, i, j + 1)) { // 往右走 return true; } else if (findWay(map, i - 1, j)) { // 往上走 return true; } else if (findWay(map, i, j - 1)) { // 往左走 return true; } else { map[i][j] = 3; // 死路标记为3 return false; } } else { // map[i][j] = 1(障碍物)、2(原路返回)、3(死路) return false; } } } } /* 初始二维数组如下: 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 0 1 0 0 0 1 1 1 1 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 ----------分隔符----------- 方法1走过的二维数组如下: 1 1 1 1 1 1 1 1 2 2 2 0 0 1 1 3 1 2 0 0 1 1 1 1 2 0 0 1 1 0 0 2 0 0 1 1 0 0 2 0 0 1 1 0 0 2 2 2 1 1 1 1 1 1 1 1 */
public class Maze {
public static void main(String[] args) {
// 0表示可以走、1表示不能走、2表示走过、3表示走过了,发现是死路、当map[6][5]为2时成功走出迷宫
int[][] map = new int[8][7];
// 最上面与最下面两行设置为1
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}
// 最左边和最右边两行设置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
map[2][2] = 1;
// 打印初始二维数组
System.out.println("初始二维数组如下:");
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "\t");
}
System.out.println();
}
// 方法2(右、下、上、左)
System.out.println("----------分隔符-----------");
findWayTwo(map, 1, 1);
System.out.println("方法2走过的二维数组如下:");
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "\t");
}
System.out.println();
}
}
// 方法2(右、下、上、左)
public static boolean findWayTwo(int[][] map, int i, int j) { // i、j分别表示行、列
if (map[6][5] == 2) {
return true;
} else {
if (map[i][j] == 0) {
map[i][j] = 2; // 假定当前的位置为走过,且走得通
if (findWayTwo(map, i, j+1)) { // 往右走
return true;
} else if (findWayTwo(map, i+1, j )) { // 往下走
return true;
} else if (findWayTwo(map, i - 1, j)) { // 往上走
return true;
} else if (findWayTwo(map, i, j - 1)) { // 往左走
return true;
} else {
map[i][j] = 3; // 死路标记为3
return false;
}
} else { // map[i][j] = 1(障碍物)、2(原路返回)、3(死路)
return false;
}
}
}
}
/*
初始二维数组如下:
1 1 1 1 1 1 1
1 0 0 0 0 0 1
1 0 1 0 0 0 1
1 1 1 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 0 0 0 0 0 1
1 1 1 1 1 1 1
----------分隔符-----------
方法2走过的二维数组如下:
1 1 1 1 1 1 1
1 2 2 2 2 2 1
1 0 1 0 0 2 1
1 1 1 0 0 2 1
1 0 0 0 0 2 1
1 0 0 0 0 2 1
1 0 0 0 0 2 1
1 1 1 1 1 1 1
*/
-
汉诺塔。
package methodtest.methodcursion; import java.util.Scanner; public class HannoTower { public static void main(String[] args) { Scanner myScanner = new Scanner(System.in); System.out.print("请输入第一个柱子里的盘子个数:"); int num = myScanner.nextInt(); hannoTower(num, 'a', 'b', 'c'); } public static void hannoTower(int num, char a, char b, char c) { // num为第一个柱子还剩下的石头数量 if (num == 1) { System.out.println(a + " -> " + c); // 第一个柱子剩下最后一个石头时,把最后一个石头移动到第三个柱子 } else { hannoTower(num - 1, a, c, b); // 第一步,把第一个柱子的(n-1)个石头移动到第二个柱子 System.out.println(a + " -> " + c); hannoTower(num - 1, b, a, c); // 要想完成第一步。要把第二个柱子的(n-1)个石头先移动到第三个柱子 } } }
-
八皇后:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
4.3.5.2 递归重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
- 方法的局部变量是独立的,不会相互影响。
- 如果方法中使用的是引用数据类型变量(比如数组、对象),就会共享该引用类型的数据。
- 递归必须向退出递归的条件逼近,否则就是无限递归。
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法就执行完毕。
4.3.6 构造器(Constructor)
- 概述:构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。
4.3.6.1 构造器的细节
- 构造器与类同名。
- 每个类可以有一个以上的构造器,一但定义了自己的构造器,默认的构造器就被覆盖了,就不能再使用默认的无参构造器,除非显式的定义一下。
- 构造器可以有 0 个、1 个或多个参数。
- 构造器没有返回值。
- 默认构造方法的访问修饰符和类的访问修饰符相同(类为 public,构造函数也为 public;类改为 protected,构造函数也改为 protected。
- 构造器总是伴随着 new 操作一起调用,即构造器是完成对象的初始化,而不是创建对象。
- 构造器与其他的方法有一个重要的不同。构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。
- 不要在构造器中定义与实例域重名的局部变量。
- 可以将实例域定义为 final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。例如,可以将 Employee 类中的 name 域声明为 final, 因为在对象构建之后,这个值不会再被修改, 即没有 setName 方法。
4.3.6.2 构造器练习
-
public class ConstructorTest01 { public static void main(String[] args) { Person person1 = new Person(); Person person2 = new Person(10, "cxk"); System.out.println("person1的年龄为:" + person1.age + ",名字为:" + person1.name); // person1的年龄为:18,名字为:null System.out.println("person2的年龄为:" + person2.age + ",名字为:" + person2.name); // person2的年龄为:10,名字为:cxk } } class Person { public int age; public String name; public Person() { // 无参构造器 this.age = 18; } public Person(int age, String name) { // 全参构造器 this.age = age; this.name = name; } }
4.3.6.3 对象创建过程中构造器的作用
4.4 静态域与静态方法
4.4.1 静态域
- 概述:如果将域定义为 static, 每个类中只有一个这样的域。而每一个对象对于所有的实例域,却都有自己的一份拷贝。例如, 假定需要给每一个雇员賦予唯一的标识码。这里给 Employee类添加一个实例域 id 和一个静态域 nextld。即使没有一个雇员对象, 静态域 nextld 也存在。它属于类,而不属于任何独立的对象。
4.4.2 静态常量
-
概述:静态变量使用得比较少,但静态常量却使用得比较多。例如, 在 Math 类中定义了一个静态常量:
public class Hath { public static final double PI = 3.14159265358979323846; }
4.4.3 静态方法
- 静态方法是一种不能向对象实施操作的方法。例如, Math 类的 pow 方法就是一个静态方法。表达式:Math.pow(x, a)
- 在下面两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显示参数提供(Math.pow)。
- 一个方法只需要访问类的静态域(例如:Employee.getNextId。
- main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main 方法将执行并创建程序所需要的对象。
4.5 包(package)
- 定义:Java 允许使用包( package > 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。使用包的主要原因是确保类名的唯一性。
- 从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.util 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合。
4.5.1 包的命名
-
命名规范:
-
只能包含数字、字母、下划线、小圆点,但不能用数字开头,不能是关键字或保留字。
-
一般是小写字母 + 小圆点,如com.公司名.项目名.业务模块名。
-
4.5.2 类的导入(import)
-
第一种方式:在每个类名之前添加完整的包名(较为繁琐,不推荐)。例如:
java.tiie.LocalDate today = java.tine.Local Date.now();
-
第二种方式:使用import语句(推荐)。
import java.util .*; // 导入java.util包下的所有类 import java.time.LocalDate; // 导入java.util包下的LocalDate类
-
import指令放在package的下面,在类定义的前面,可以有多句且没有顺序要求。
-
静态导入(import static):使用 import 可以省略写包名,而使用 import static 可以省略类名。
-
语法格式:
import static package.ClassName.fieldName|methodName;
-
使用案例:
import static java.lang.System.*; import static java.lang.Math.*; public class StaticImportTest { public static void main(String[] args) { // out是java.lang.System类的静态成员变量,代表标准输出 // PI是java.lang.Math类的静态成员变量,表示π常量 out.println(PI); // 直接调用Math类的sqrt静态方法,返回256的正平方根 out.println(sqrt(256)); } }
-
4.5.3 将类放入包中(package)
-
要想将一个类放人包中, 就必须将包的名字放在源文件的开头,包中定义类的代码之前。
-
package的作用是声明当前类所在的包,需要放在类的最上面,一个类中最多只有一句package。
4.5.4 包的使用细节
-
发生命名冲突的时候,就不能不注意包的名字了。例如,java.util 和 java.sql 包都有日期( Date) 类。如果在程序中导入了这两个包:如果这两个 Date 类都需要使用,又该怎么办呢? 答案是,在每个类名的前面加上完整的包名。
import java.util .*; import java.sql .*; java.util.Date deadline = new java.util.Date(); java.sql.Date today = new java.sql.Date(...);
-
4.6 访问修饰符(Modifier)
-
定义:Java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围)。
-
1 访问级别 访问控制修饰符 同类 同包 子类 不同包 2 公开 public ✔ ✔ ✔ ✔ 3 受保护 protected ✔ ✔ ✔ ❌ 4 默认 无修饰符 ✔ ✔ ❌ ❌ 5 私有 private ✔ ❌ ❌ ❌
4.6.1 注意事项
- 修饰符可以用来修饰类中的属性,成员方法以及类。
- 只有默认的和public才能修饰类,并且遵循上述访问权限的特点。