6.2.3 引用变量
public class Main { public static void main(String[] args) { int[] arr1 = new int[3]; arr1[0] = 1; arr1[1] = 2; arr1[2] = 3; int[] arr2 = new int[]{1,2,3,4,5}; arr2[0] = 100; arr2[1] = 200; arr1 = arr2; arr1[2] = 300; arr1[3] = 400; arr2[4] = 500; for (int x : arr1) { System.out.println(x); } } }
6.2.4 认识 null
null
在 Java 中表示“空引用”,也就是一个不指向对象的引用
public class Main { public static void main(String[] args) { int[] arr = null; System.out.println(arr[0]); } }
null
的作用类似于C语言中的NULL
(空指针),都是表示一个无效的内存位置。因此不能对这个内存进行任何读写操作。一旦尝试读写,就会抛出NullPointerException
【注意】:Java 中并没有约定
null
和 0 下标地址的内存有任何关联。
6.3 数组应用场景
6.3.1 保存数据
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3}; for (int x : arr) { System.out.println(x); } } }
6.3.2 作为方法的参数
- 参数传基本数据类型
public class Main { public static void main(String[] args) { int num = 0; func(num); System.out.println("num = " + num);// 0 } private static void func(int x) { x = 10; System.out.println("x = " + x); // 10 } }
- 上述代码我们可以发现
func
方法中修改了形参x
的值,不影响实参的num
值。 - 参数传引用数据类型
public class Main { public static void main(String[] args) { int[] arr = new int[]{1,2,3}; fun1(arr); System.out.println(Arrays.toString(arr)); // [1,2,3] fun2(arr); System.out.println(Arrays.toString(arr)); // [99,2,3] } public static void fun1(int[] arr) { arr = new int[]{11,22,33,44,55}; // 修改了形参的指向 } public static void fun2(int[] arr) { arr[0] = 99; // 形参改变了实惨的值 } }
上述代码我们可以发现fun1方法中修改了形参的指向,不影响实参数组的值
fun2方法内部修改了数组的内容,方法外部的数组内容也发生了改变。因为数组是引用类型,按照引用类型进行传递,是可以修改其中存放的内容的。
【总结】:所谓的“引用”本质只是存了地址。Java 将数组设定为引用类型,这样的话后续进行数组参数传参,其实只是将数组的地址传入函数形参中,这样可以避免对整数数组的拷贝(数组可能比较长,那么拷贝开销就会很大)。
6.3.3 作为方法的返回值
public class Main { public static void main(String[] args) { int[] ret = fun(); System.out.println(Arrays.toString(ret)); // [1, 2, 3, 4, 5] } public static int[] fun() { int[] arr = new int[]{1,2,3,4,5}; return arr; } }
6.4 二维数组
二维数组本质上也就是一维数组,只不过每个元素又是一个一维数组
基本语法:
数据类型[][] 数组名称 = new 数据类型 [行数][列数] { 初始化数据 };
代码示例:
public class Main { public static void main(String[] args) { int[][] arr = {{1, 2, 3},{4,5,6}}; for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr[i].length; j++) { System.out.print(arr[i][j] + " "); } System.out.println(); } System.out.println("======="); for (int[] tempArr : arr) { for (int x : tempArr) { System.out.print(x + " "); } System.out.println(); } System.out.println("======="); String ret = Arrays.deepToString(arr); // deepToString()深度打印 System.out.println(ret); } }
Java 二维数组在定义的时候是可以省略列的
int[][] arr = new int[2][];
二维数组的用法和一维数组并没有明显差别, 因此我们不再赘述.
同理, 还存在 “三维数组”, “四维数组” 等更复杂的数组, 只不过出现频率都很低.
6.5 不规则数组
代码示例:
public class Main { public static void main(String[] args) { int[][] arr = new int[2][]; // 每一个一维数组 进行初始化 arr[0] = new int[3]; arr[1] = new int[5]; for (int i = 0; i < arr.length; i++) { for (int j = 0; j < arr[i].length; j++) { System.out.print(arr[i][j] + " "); } System.out.println(); } } } // 运行结果 0 0 0 0 0 0 0 0
7. 类和对象
7.1 面向对象程序设计概述
面向对象程序设计(object-oriented programming,OOP),是当今主流的程序设计范畴,它取代了20世纪70年代的”结构化“或过程式编程技术。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特点功能部分和隐藏的实现部分。
7.1.1 类
类(class)是构造对象的模板或蓝图。由类构造(construct)对象的过程称为类的实例(instance)。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象的数据称为实例字段(instance field),操作数据的过程称为方法(method)。
实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,这是提高重用性和可靠性的关键。这意味着一个类可以完全改变存储数据的方式,只要仍旧使用同样的方法操作数据,其他对象就不会知道也不用关心这个类所发生的变化。
7.1.2 对象
要想使用OOP,一定要清楚对象的三个主要特性:
对象的行为(behavior)——可以对对象完成哪些操作,或者可以对对象应用哪些方法?
对象的状态(state)—当调用那些方法时,对象会如何响应?
对象的标识(identity)——如何区分具有相同行为与状态的不同对象?
同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是可调用的方法来定义的。
7.1.3 类之间的关系
在类之间,最常见的关系有:
- 依赖(uses-a);
- 聚合(has-a);
- 继承(is-a)。
依赖(dependence),即“uses-a”关系,是一种最明显、最常见的关系。如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。
聚合(aggregation),即“has-a”关系,很容易理解,因为这种关系很具体。包含关系意味着类A的对象包含类B的对象。
继承(inheritance),即“is-a”关系,表示一个更特殊的类与一个更一般类之间的关系。
7.2 类的定义和使用
面向对象程序设计关注的是对象,而对象是现实生活中的实体。
7.2.1 简单认识类
类是用来对一个实体(对象)来进行描述,主要描述该实体(对象)具有哪些属性,哪些功能,描述完成后计算机就可以识别了。
7.2.2 类的定义格式
在java
中定义 类需要使用class
关键字,具体语法如下:
class ClassName{ field;// 字段 或 成员变量 method;// 方法 或 成员方法 }
class
为定义类的关键字,ClassName
为类的名字,{}
中为类的主体。
类中包含的内容称为类的成员。属性主要是用来描述类的,称之为类的成员属性或者类**成员变量**。方法主要说明类具有哪些功能,称为类的成员方法。
7.2.3 自定义类
7.2.3.1 定义一个狗类
class Dog { // 狗的属性 public String name;// 狗的名字 public String color;// 狗的颜色 // 狗的行为 public void barks() { System.out.println(name + "在旺旺叫"); } public void wag() { System.out.println(name + "在摇尾巴"); } }
注意事项:
- 一般一个文件当中只定义一个类。
main
方法所在的类一般要使用public
修饰。public
修饰的类必须要和文件相同。
7.3 类的实例化
7.3.1 什么是实例化
定义一个类,就相当于在计算机中定义了一种新的类型,与int,double类似,只不过int和double是java语言自带的内置类型,而类是用户自定义了一个新的类型,比如上述的Dog类。它就是类(一种新定义的类型)有了这些自定义的类型之后,就可以使用这些类来定义实例(或者称为对象)。
用类类型创建对象的过程,称为类的实例化,在java采用new关键字,配合类名来实例化对象。
public class Test { public static void main(String[] args) { Dog dog1 = new Dog(); dog1.name = "大黄"; dog1.color = "黄色"; dog1.barks(); dog1.wag(); Dog dog2 = new Dog(); dog2.name = "哈士奇"; dog2.color = "白黑色"; dog2.barks(); dog2.wag(); } } // 运行结果: 大黄在旺旺叫 大黄在摇尾巴 哈士奇在旺旺叫 哈士奇在摇尾巴
注意事项:
new
关键字用于创建一个对象的实例。- 使用
.
来访问对象中属性和方法。 - 同一个类可以创建多个实例。
7.3.2 类和对象的说明
类只是一个模型一样的东西,用来对一个实体进行描述,限定了类有哪些成员。
类是一种自定义的类型,可以用来定义常量
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
做个比方,类的实例化出对象就像现实中使用建筑设计圈造出房子,类就是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化的对象才能实际存储数据,占用物理空间。
7.4 this 引用
7.4.1 为什么要使用this引用
先看一个日期类的例子:
class Date{ public int year; public int month; public int day; public void setDate(int y, int m, int d){ year = y; month = m; day = d; } public void printDate(){ System.out.println(year + "/" + month + "/" + day); } } public class Test2 { public static void main(String[] args) { // 构造三个日期类型的对象 d1 d2 d3 Date d1 = new Date(); Date d2 = new Date(); Date d3 = new Date(); // 对d1,d2,d3的日期设置 d1.setDay(2020,9,15); d2.setDay(2020,9,16); d3.setDay(2020,9,17); // 打印日期中的内容 d1.printDate(); d2.printDate(); d3.printDate(); } }
以上代码定义了一个日期类,然后main方法中创建了三个对象,并通过Date类中的成员方法对对象进行设置和打
印,代码整体逻辑非常简单,没有任何问题。
但是细思之下有以下两个疑问:
形参名不小心与成员变量名相同
public void setData(int year, int month, int day){ year = year; month = month; day = day; }
那函数体中到底是谁给谁赋值?成员变量给成员变量?参数给参数?参数给成员变量?成员变量参数?估计
自己都搞不清楚了。
三个对象都在调用setDate和printDate函数,但是这两个函数中没有任何有关对象的说明,setDate和
printDate函数如何知道打印的是那个对象的数据呢?
7.4.2 什么是this引用
this引用指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该引用去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
public void setData(int year, int month, int day){ this.year = year; this.month = month; this.day = day; }
注意:this引用的是调用成员方法的对象。
public class Test { public static void main(String[] args) { Date d = new Date(); d.setData(2020,9,15); d.printDate(); } }
7.4.3 this引用的特性
this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型。
this只能在“成员方法”中使用。
在"成员方法"中,this只能引用当前对象,不能再引用其他对象。
this是“成员方法”第一个隐藏的参数,编译器会自动传递,在成员方法执行时,编译器负责将调用成员方法对象的引用传递给该成员方法,this负责来接收。
7.5 对象的构造及初始化
7.5.1 如何初始化对象
在java
方法内部定义一个局部变量,必须初始化,否则就会编译失败。
要上诉代码编译成功,只需要是在使用a
之前,给a
设置一个初始值。如果是对象:
public class Test { public static void main(String[] args) { Date d = new Date(); d.setData(2020,9,15); d.printDate(); } } // 代码正常通过编译
需要调用之前写的setDate
方法才可以将具体的日期设置到对象中。通过上述例子发现两个问题:
- 每次对象创建好后调用
setDate
方法设置具体日期,比较麻烦,那对象该如何初始化? - 局部变量必须要初始化才能使用,为什么字段声明之后没有给值依然可以使用?
7.5.2 构造方法
7.5.2.1 概念
构造方法(也称为构造器)是一种特殊的成员方法,名字必须与类名相同,在创建对象时,编译器自动调用并且在整个对象的生命周期内调用一次。
class Date{ public int year; public int month; public int day; // 构造方法 public Date(int year, int month, int day){ this.year = year; this.month = month; this.day = day; System.out.println("Date(int year, int month, int day)被调用了"); } public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test { public static void main(String[] args) { Date d = new Date(2020,9,15); d.printDate(); } }
注意:构造方法的作用就是对对象中的成员进行初始化,并不负责给对象开辟空间。
7.5.2.2 特性
- 构造器与类同名
- 每一个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着
new
操作符一起调用 - 构造方法可以重载
class Date{ public int year; public int month; public int day; // 构造方法 // 无参构造方法 public Date() { } // 带3个参数的构造方法 public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test { public static void main(String[] args) { Date d = new Date(2020,9,15); d.printDate(); } } // 上述两个构造方法:名字相同,参数列表不同,因此构成了方法重载。
如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的
class Date{ public int year; public int month; public int day; public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test { public static void main(String[] args) { Date d = new Date(); d.printDate(); } }
- 上述
Date
类,没有定义任何构造方法,编译器会默认生成一个无参构造器。
注意:一旦用户定义,编译器就不会生成
- 构造方法中,可以通过this调用其他构造方法来简化代码
class Date{ public int year; public int month; public int day; public Date() { this(2005,5,9);// 必须是构造方法的第一条语句 } public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test2 { public static void main(String[] args) { Date d = new Date(); d.printDate(); } }
- 注意:this(…)必须是构造方法的第一条语句。
7.5.3 默认初始化
在上文中提出的第二个问题:为什么局部变量在使用时必须要初始化,而成员变量可以不用呢?
class Date{ public int year; public int month; public int day; public Date(int year, int month, int day) { // 成员变量在定义时,并没有给初始值, 为什么就可以使用呢? System.out.println(this.year); System.out.println(this.month); System.out.println(this.day); } public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test2 { public static void main(String[] args) { Date d = new Date(2023,9,17); } }
要搞清楚这个过程,就需要知道new
关键字背后所发生的一些事情:
Date d = new Date(2023,9,17);
在程序层面只是简单的一条语句,在JVM层面需要做好多事情,下面简单介绍一下:
- 检测对象对应的类是否加载了,如果没有加载则加载
- 为对象分配内存空间
- 处理并发安全问题
比如:多个线程同时申请对象,JVM
要保证给对象分配的空间不冲突
初始化所分配的空间
即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:
数据类型 | 默认值 |
byte | 0 |
char | ‘\u0000’ |
short | 0 |
int | 0 |
long | 0L |
boolean | false |
float | 0.0f |
double | 0.0 |
reference | null |
- 设置对象头信息
- 调用构造方法,给对象中各个成员赋值
7.5.4 就地初始化
在声明成员变量的时候,就可以给出初始值。
class Date{ public int year = 2021; public int month = 5; public int day = 19; public Date() { } public Date(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } public void printDate(){ System.out.println(this.year + "/" +this.month + "/" + this.day); } } public class Test2 { public static void main(String[] args) { Date d = new Date(2023,9,17); d.printDate(); Date d1 = new Date(); d1.printDate(); } } // 运行结果 // 2023/9/17 // 2021/5/19
注意:代码编译完成后,编译器会将所有给成员初始化的这些语句添加到各个构造函数中。
8. 封装、继承和多态
面向对象三大特性:封装、继承和多态。
8.1 封装
8.1.1 封装的概念
在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。
封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
要访问该类的代码和数据,必须通过严格的接口控制。
封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。
适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。
8.1.2 为什么封装
封装的目的是保护数据的安全和完整性,同时隐藏数据的实现细节,提高代码的可维护性和可扩展性,具体有以下几个方面的好处:
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员变量进行更精确的控制。
- 隐藏信息,实现细节。
8.1.3 封装的实现步骤
- 修改属性的可见性来限制属性的访问(用
private
来修饰),如:
public class Test { private String name; private int age; }
- 这段代码中,将
name
和age
属性设置为私有的,只有在本类中被访问,其他类访问不了,就实现对信息的隐藏。 - 对每个值属性提供对外的公共方法访问,也就是创建一对赋取值方法,用于对私有属性的访问,例如:
public class Test { private String name; private int age; 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; } }
- 采用
this
关键字是为了解决实例变量和局部变量之间发生的同名的冲突。
8.2 继承
8.2.1 继承的概念
继承是 java
面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
生活中的继承
兔子和羊属于食草动物,狮子和老虎属于食肉动物。
食草动物和食肉动物又是属于动物。
所有的继承需要符合的关系:is-a,父类更通用,子类更具体。
虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。
8.2.2 继承的格式
在 Java
中通过关键字 extends
来声明一个类是从另一个类继承而来的,一般格式如下:
class 父类 { } class 子类 extends 父类 { }
8.2.3 为什么继承
接下来我们通过实例的说明这个需求。
开发动物类,其中动物分别为狗和猫,要求如下:
- 狗:属性(姓名,颜色),方法(吃,叫)
- 猫:属性(姓名,颜色),方法(吃,叫)
class Dog{ private String name; private String color; public void eat() { System.out.println(this.name + "正在吃..."); } public void cry() { System.out.println(this.name + "正在叫..."); } }
class Cat { private String name; private String color; public void eat() { System.out.println(this.name + "正在吃..."); } public void cry() { System.out.println(this.name + "正在叫..."); } }
从这两段代码可以看出来,代码存在重复了,导致后果就是代码量大且臃肿,而且维护性不高(维护性主要是后期需要修改的时候,就需要修改很多的代码,容易出错),所以要从根本上解决这两段代码的问题,就需要继承,将两段代码中相同的部分提取出来组成 一个父类:
class Animal { private String name; private String color; public Animal(String name, String color) { this.name = name; this.color = color; } public void eat() { System.out.println(this.name + "正在吃..."); } public void cry() { System.out.println(this.name + "正在叫..."); } }
这个Animal类就可以作为一个父类,然后狗类和猫类继承这个类之后,就具有父类当中的属性和方法,子类就不会存在重复的代码,维护性也提高,代码也更加简洁,提高代码的复用性(复用性主要是可以多次使用,不用再多次写同样的代码) 继承之后的代码:
- 狗类
class Dog extends Animal{ public Dog(String name , String color) { super(name, color); } }
- 猫类
class Cat extends Animal { public Cat(String name , String color) { super(name, color); } }
8.2.4 继承类型
Java
中不支持多继承,但支持多重继承。
8.2.5 继承特性
子类拥有父类非 private 的属性、方法。
子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
子类可以用自己的方式实现父类的方法。
Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 A 类继承 B 类,B 类继承 C 类,所以按照关系就是 C 类是 B 类的父类,B 类是 A 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
8.2.6 super 与 this 关键字
super
关键字:我们可以通过super
关键字来实现对父类成员的访问,用来引用当前对象的父类
this
关键字:指向自己的引用。
class Animal { void eat() { System.out.println("animal : eat"); } } class Dog extends Animal { void eat() { System.out.println("dog : eat"); } void eatTest() { this.eat(); // this 调用自己的方法 super.eat(); // super 调用父类方法 } } public class Test { public static void main(String[] args) { Animal a = new Animal(); a.eat(); Dog d = new Dog(); d.eatTest(); } } // 运行结果 //animal : eat //dog : eat //animal : eat
8.2.7 final 关键字
final
关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写;
- 声明类
final class 类名 { // 类体 }
- 声明方法
访问限定符 final 返回值类型 方法名(){ // 方法体 }
注意:实例变量也可以被定义为final
,被定义为final
的变量不能被修改。被声明为final
的类的方法自动声明为final
,但是实例变量并不是final
。
8.3 多态
8.3.1 多态的概念
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作,如图所示:
多态性是对象多种表现形式的体现。
同一个事件发生在不同的对象上会产生不同的结果。
8.3.2 多态的优点
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
8.3.3 多态存在的三个必要条件
- 继承
- 重写
重写:子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。即外壳不变,核心重写。
重写的好处:在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
class Animal { public void eat() { System.out.println("正在吃..."); } } class Dog extends Animal { public void eat() { System.out.println("狗正在吃狗粮..."); } } class Bird extends Animal { public void eat() { System.out.println("鸟正在吃鸟粮..."); } } public class Test1 { public static void fun(Animal animal) { animal.eat(); } public static void main(String[] args) { Dog dog = new Dog(); fun(dog); Bird bird = new Bird(); fun(bird); } } // 运行结果 狗正在吃狗粮... 鸟正在吃鸟粮...
- 重写(覆盖)的规则:
- 方法名相同
- 参数列表相同【顺序、个数、类型】
- 返回值相同
- 父类引用指向子类对象
比如:
Animal dog = new Dog();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
以下是多态的例子:
abstract class Animal { abstract void eat(); } class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } public void work() { System.out.println("抓老鼠"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } public void work() { System.out.println("看家"); } } // 测试类 public class Test { public static void show(Animal a) { a.eat(); // 类型判断 if (a instanceof Cat) { // 猫做的事情 Cat c = (Cat)a; c.work(); } else if (a instanceof Dog) { // 狗做的事情 Dog c = (Dog)a; c.work(); } } public static void main(String[] args) { show(new Cat()); // 以 Cat 对象调用 show 方法 show(new Dog()); // 以 Dog 对象调用 show 方法 Animal a = new Cat(); // 向上转型: 子类对象 -> 父类对象 a.eat(); // 调用的是 Cat 的 eat Cat c = (Cat)a; // 向下转型: 父类对象 -> 子类对象 c.work(); // 调用的是 Cat 的 work } } // 运行结果 吃鱼 抓老鼠 吃骨头 看家 吃鱼 抓老鼠
8.3.4 instanceof 关键字
Java中可以使用instanceof
关键字判断对象是否是某个类的实例,语法格式如下:
对象 instanceof 类
在上述格式中,如果对象是指定类的实例对象,则返回true
,否则返回false
。
class Animal { public String name; public int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(this.name + "正在吃..."); } } class Dog extends Animal { public String color; public Dog(String name, int age,String color) { super(name,age); this.color = color; } public void eat() { System.out.println(this.name + "正在吃狗粮..."); } public void barks() { System.out.println(this.name + "正在旺旺叫..."); } } class Bird extends Animal { public Bird(String name, int age) { super(name, age); } public void eat() { System.out.println(this.name + "正在吃鸟粮..."); } public void fly() { System.out.println(this.name + "正在飞..."); } } public class Test1 { public static void main(String[] args) { Animal animal1 = new Dog("旺财",10,"黄色"); if (animal1 instanceof Bird){ // 判断 dog对象是否是Bird类的实例 如果是则实例化对象,否则打印hell Bird bird2 = (Bird)animal1; bird2.fly(); }else { System.out.println("hell"); } } }
9. 接口和抽象类
9.1 抽象类
9.1.1 抽象类的概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是放过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
9.1.2 抽象类的语法
在Java 中,一个类如果被abstract
修饰的类成为抽象类,抽象类中被abstract
修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。
抽象类的定义格式如下:
abstract class 抽象类名{ 属性; // 普通方法 访问权限 返回值类型 方法名称(参数){ return [返回值]; } // 抽象方法,无方法体 访问权限 abstract 返回值类型 抽象方法名称(参数); }
从以上格式可以看出,抽象类的定义比普通类多了抽象方法,类的其他功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
9.1.3 抽象类的特性
- 抽象类不能直接实例化对象
- 抽象方法不能是被
private
修饰的
- 抽象方法不能被
final
和static
修饰,因为抽象方法要被子类重写。
抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract
修饰,如果一个非抽象类继承了抽象类,那么这个子类必须实现抽象类中的全部抽象方法。
abstract class Shape { // 抽象方法 public abstract void draw(); } abstract class A extends Shape { public abstract void testA(); } class B extends A { @Override public void testA() { } @Override public void draw() { } }
- 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
- 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
9.2 接口
接口用来描述类应该做什么,而不指定它们具体应该如何做。一个类可以实现一个或多个接口。有些情况可能要求符合这些接口,主要有这种要求,就可以使用实现了这个接口的类(即实现类)的对象。
9.2.1 接口的概念
在Java程序设计语言中,接口不是类,而是对希望符合这个接口的类的一组需求。
接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
9.2.2 接口的语法规则
接口的定义格式与定义类的格式基本相同,将class
关键字换成interface
关键字,就定义了一个接口。
public interface 接口名称{ // 抽象方法 }
注意事项:
- 创建接口时,接口的命名一般以大写字母
I
开头- 接口命名一般使用“形容词”词性的单词。
9.2.3 接口的使用
接口不能直接被使用,必须有一个"实现类"来实现该接口,实现接口的所有的抽象方法。
public class 类名 interface 接口名称{ // 可以使用,分隔,实现多个接口 // ... }
注意:子类和父类之间是extends
继承关系,类与接口之间是implements
实现关系。
9.2.4 接口的特性
- 接口类型是一种引用类型,但是不能直接
new
接口的对象
接口中每一个方法都是public
的抽象方法, 即接口中的方法会被隐式的指定为 public abstract
(只能是
public abstrac
t,其他修饰符都会报错)
- 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现
- 重写接口中方法时,不能使用默认的访问权限
- 接口中可以含有变量,但是接口中的变量会被隐式的指定为
public static final
变量
- 接口中不能有静态代码块和构造方法(编译错误)
- 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是
.class
- 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
9.2.5 实现多个接口
在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。下面通过类来表示一组动物。
abstract public class Animal { public String name; public Animal(String name) { this.name = name; } }
另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”。
interface IFly { void fly(); } interface IRun { void run(); } interface ISwim { void swim(); }
接下来我们创建几个具体的动物:
猫, 是会跑的。
public class Cat extends Animal implements IRun{ public Cat(String name) { super(name); } @Override public void run() { System.out.println(this.name + "正在用四条腿跑"); } }
鱼,是会游泳的
public class Fish extends Animal implements ISwim{ public Fish(String name) { super(name); } @Override public void swim() { System.out.println(this.name + "正在游泳"); } }
青蛙,既能跑,又能游泳(两栖动物)
public class Fish extends Animal implements IRun,ISwim{ public Fish(String name) { super(name); } @Override public void run() { System.out.println(this.name + "正在用两条腿跑"); } @Override public void swim() { System.out.println(this.name + "正在游泳"); } }
注意:一个类实现多个接口,每个接口的抽象方法都要实现,否则类必须设置为抽象类
而鸭子即可以飞,又能跑、还可以游泳。
public class Duck extends Animal implements IFly,IRun,ISwim{ public Duck(String name) { super(name); } @Override public void fly() { System.out.println(this.name + "正在用两个翅膀飞"); } @Override public void run() { System.out.println(this.name + "正在用两条腿跑"); } @Override public void swim() { System.out.println(this.name + "正在用两条腿游泳"); } }
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。
继承表达的含义是 is-a 语义, 而接口表达的含义是 具有 xxx 特性 .
猫是一种动物,具有跑的特性。
鱼是一种动物,具有游泳的特性。
青蛙是一种,具有跑和游泳的特性。
鸭子是一种动物,具有跑、游泳和飞的特性。
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力.例如:现在定义一个方法:testRun
public static void testRun(IRun iRun) { iRun.run(); }
在这个testRun
方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行。
public static void main(String[] args) { testRun(new Bird("布谷")); testRun(new Duck("唐老鸭")); testRun(new Dog("旺财")); } // 运行结果 布谷正在用两条腿跑 唐老鸭正在用两条腿跑 旺财正在用四条腿跑
甚至参数可以不是 “动物”, 只要会跑!
class Roboot implements IRun{ @Override public void run() { System.out.println("机器人正在用两条腿跑"); } } public class Test { public static void testRun(IRun iRun) { iRun.run(); } public static void main(String[] args) { testRun(new Roboot()); } } // 运行结果 机器人正在用两条腿跑
9.2.6 接口之间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。
接口可以继承一个接口, 达到复用的效果. 使用 extends
关键字.
interface IRun { void run(); } interface ISwim { void swim(); } // 两栖的动物, 既能跑, 也能游 interface IAmphibious extends IRun,ISwim{ } public class Frog extends Animal implements IAmphibious{ ... }
通过接口继承创建一个新的接口IAmphibious
表示 “两栖的”. 此时实现接口创建的 Frog
类, 就继续要实现 run
方法, 也需要实现 swim
方法.
接口间的继承相当于把多个接口合并在一起.
9.3 抽象类和接口的区别
抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别。
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.
public class Animal { public String name; public Animal(String name) { this.name = name; } }
区别 | 抽象类 | 接口 |
结构组成: | 普通类 + 抽象方法 | 抽象方法 + 静态常量 |
权限: | 各种权限 | public |
子类使用: | 使用extends 关键字继承抽象类 |
使用implements 关键字实现接口 |
关系: | 一个抽象类可以实现若干接口 | 接口不能继承抽象类,但是接口可以使用extends 关键字继承多个父接口 |
子类权限: | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
10. String字符串
在程序开发中经常会用到字符串。字符串是指一连串的字符,它是由许多单个字符连接而成的,如多个英文字母所组成的英文单词。字符串可以包含任意字符,这些字符必须包含在一对双引号""之内,例如:“abc”。Java定义了3个封装字符串的类,分别是String类、StringBuffer类和StringBulider类。它们位于java.lang 包中,并提供了一系列操作字符串的方法,这些方法不需要导包就可以直接使用。下面将对String类、StringBuffer类和StringBulider类进行讲解。
10.1 String类
10.1.1 字符串的构造
String
类提供了构造方法非常多,常用的就以下三种:
public class Main { public static void main(String[] args) { // 使用常量串构造 String s1 = "hello"; System.out.println(s1); // 直接new String对象 String s2 = new String("hello"); System.out.println(s2); // 使用字符数组进行构造 char[] chars = {'h','e','l','l','o'}; String s3 = new String(chars); System.out.println(s3); } }
其他方法需要用到时,大家参考Java在线文档:String官方文档
【注意】:
- String是引用类型,内部并不存储字符串本身,在String类的实现源码中,String类实例变量如下:
public class Main { public static void main(String[] args) { // s1和s2引用的是不同对象 s1和s3引用的是同一对象 String s1 = new String("hello"); String s2 = new String("world"); String s3 = s1; // s3这个引用指向了s1这个引用的对象 System.out.println(s3); // hello System.out.println(s1.length());// 获取字符串的长度 System.out.println(s1.isEmpty());// 如果字符串长度为0,返回true,否则返回false String s4 = ""; System.out.println(s4.length()); // 0 System.out.println(s4.isEmpty());// true }
- 内存图:
- 在Java中""引起来的也是String类型对象
// 打印"hello"字符串(String对象)的长度 System.out.println("hello".length());// 5
10.1.2 String对象的比较
字符串的比较是常见操作之一,比如:字符串排序。Java中总共提供了4种方式:
1. ==比较是否引用同一个对象
注意:对于内置类型,==
比较的是变量中的值;对于引用类型==
比较的是引用中的地址。
public class Main { public static void main(String[] args) { int a = 10; int b = 20; int c = 10; // 对于基本类型变量,==比较两个变量中存储的值是否相同 System.out.println(a == b); // false System.out.println(a == c); // true // 对于引用类型变量,==比较两个引用变量引用的是否为同一个对象 String s1 = new String("hello"); String s2 = new String("hello"); String s3 = new String("world"); String s4 = s1; System.out.println(s1 == s2); // false System.out.println(s2 == s3); // false System.out.println(s1 == s4); // true } }
2. equals()方法:按照字典序比较
字典序:字符大小的顺序
String
类重写了父类Object
中equals
方法,Object
中equals
默认按照==
比较,String
重写equals
方法后,按照 如下规则进行比较,比如:s1.equals(s2)
public class Main { public static void main(String[] args) { String s1 = new String("hello"); String s2 = new String("hello"); String s3 = new String("Hello"); // s1、s2、s3引用的是三个不同对象,因此==比较结果全部为false System.out.println(s1 == s2); // false System.out.println(s1 == s3); // false // equals比较:String对象中的逐个字符 // 虽然s1与s2引用的不是同一个对象,但是两个对象中放置的内容相同,因此输出true // s1与s3引用的不是同一个对象,而且两个对象中内容也不同,因此输出false System.out.println(s1.equals(s2)); // true System.out.println(s1.equals(s3)); // false } }
【注意】:为什么以下代码输出的结果都是true
?
答:因为在 Java 中有一块特殊的内存(常量池),存储在堆上。
它的作用是什么呢?
- 只要是""双引号括起来的字符串存放在这里。
- 存储字符串之前它会找常量池里是否存在这个字符串,如果有就不存放了(常量池不会重复存放相同的值),所以上述代码中
s1
和s2
都指向常量池hello
的地址。
3. compareTo()方法: 按照字典序进行比较
与equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。具体比较方式:
先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值
如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值
public class Main { public static void main(String[] args) { String s1 = new String("abc"); String s2 = new String("ac"); String s3 = new String("abc"); String s4 = new String("abcdef"); // s1 和 s2 比较大小 s1 > s2 返回大于0的数字 s1 < s2 返回小于0的数字 否则返回0 // 返回差值就是对应acsii码的差值 System.out.println(s1.compareTo(s2)); // 不同输出字符差值-1 System.out.println(s1.compareTo(s3)); // 相同输出 0 System.out.println(s1.compareTo(s4)); // 前k个字符完全相同,输出长度差值 -3 } }
4. 忽略大小写比较
equalsIgnoreCase()
方法:与equals()
方式相同,但是忽略大小写比较。compareToIgnoreCase()
方法:与compareTo()
方式相同,但是忽略大小写比较。
public class Main { public static void main(String[] args) { String s1 = new String("abc"); String s2 = new String("Abc"); System.out.println(s1.equals(s2)); // false System.out.println(s1.equalsIgnoreCase(s2)); // true System.out.println(s1.compareTo(s2));//32 System.out.println(s1.compareToIgnoreCase(s2));// 0 } }
10.1.3 字符串查找
字符串查找也是字符串中非常常见的操作,String类提供的常用查找的方法
方法 | 功能 |
char charAt(int index) | 返回index位置上字符,如果index为负数或者越界,抛出 IndexOutOfBoundsException异常 |
int indexOf(int ch) | 返回ch第一次出现的位置,没有返回-1 |
int indexOf(int ch, int fromIndex) | 从fromIndex位置开始找ch第一次出现的位置,没有返回-1 |
int indexOf(String str) | 返回str第一次出现的位置,没有返回-1 |
int indexOf(String str, int fromIndex) | 从fromIndex位置开始找str第一次出现的位置,没有返回-1 |
int lastIndexOf(int ch) | 从后往前找,返回ch第一次出现的位置,没有返回-1 |
int lastIndexOf(int ch, int fromIndex) | 从fromIndex位置开始找,从后往前找ch第一次出现的位置,没有返 回-1 |
int lastIndexOf(String str) | 从后往前找,返回str第一次出现的位置,没有返回-1 |
int lastIndexOf(String str, int fromIndex) | 从fromIndex位置开始找,从后往前找str第一次出现的位置,没有返 回-1 |
public class Main { public static void main(String[] args) { String s1 = new String("hello"); // 返回字符串对应下标的字符 System.out.println(s1.charAt(1)); // e //返回对应字符出来的下标位置 从头开始查找 System.out.println(s1.indexOf('e')); // 1 //返回对应字符出来的下标位置 从指定位置查找 System.out.println(s1.indexOf('l', 3)); // 3 // 字符串查找 从一个字符串找另一个字符串 System.out.println(s1.indexOf("llo")); // 2 System.out.println(s1.indexOf("ll", 2));// 2 // 返回对应字符出来的下标位置 从尾开始向前查找 System.out.println(s1.lastIndexOf('l'));// 3 // 返回对应字符出来的下标位置 从指定位置向前查找 System.out.println(s1.lastIndexOf('l', 1));// -1 System.out.println(s1.lastIndexOf("ll")); // 2 System.out.println(s1.indexOf("ll", 1));// 2 } }
10.1.4 转换
1. 数值和字符串转化
public class Main { public static void main(String[] args) { // 数字转字符串 String s1 = String.valueOf(123); System.out.println(s1); String s2 = String.valueOf(12.34); System.out.println(s2); String s3 = String.valueOf(true); System.out.println(s3); // 字符串转数字 int num1 = Integer.parseInt("1234"); System.out.println(num1); double num2 = Double.parseDouble("12.34"); System.out.println(num2); } }
2. 大小写转化
public class Main { public static void main(String[] args) { // 小写转大写 String s1 = "hello"; System.out.println(s1.toUpperCase()); // 大写转小写 String s2 = "HELLO"; System.out.println(s2.toLowerCase()); } }
问题:转化为大写/小写是在原来的字符串上进行修改的?
答:不是!!!,转化为大写/小写之后,是产生了一个新的对象
通过String
类源码中的toUpperCase()
方法和toLowerCase()
方法返回的都是一个新的字符串。
验证:
3. 字符串转数组
public class Main { public static void main(String[] args) { // 字符串转数组 String s1 = "hello"; char[] chars = s1.toCharArray(); for (char ch : chars) { System.out.println(ch); } // 数组为字符串 String s2 = new String(chars); System.out.println(s2); } }
4. 格式化
public class Main { public static void main(String[] args) { String s1 = String.format("%d-%d-%d",2021,5,19); System.out.println(s1); } }
10.1.5 字符串替换
使用一个指定的新的字符串替换掉已有的字符串数据,可用的方法如下:
方法 | 说明 |
String replaceAll(String regex, String replacement) | 替换所有的指定内容 |
String replaceFirst(String regex, String replacement) | 替换收个内容 |
public class Main { public static void main(String[] args) { String s1 = "abcabcdeabcd"; System.out.println(s1.replace('a', 'p')); // pbcpbcdepbcd System.out.println(s1.replace("ab","haha")); // hahachahacdehahacd System.out.println(s1.replaceAll("ab", "uuu")); // uuucuuucdeuuucd System.out.println(s1.replaceFirst("ab", "ha")); // hacabcdeabcd } }
10.1.6 字符串拆分
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。
方法 | 功能 |
String[] split(String regex) | 将字符串全部拆分 |
String[] split(String regex, int limit) | 将字符串以指定的格式,拆分为limit组 |
public class Main { public static void main(String[] args) { String s1 = "name = zhangsan&age = 18"; String[] strings = s1.split("&"); for (int i = 0; i < strings.length; i++) { System.out.println(strings[i]); } String s2 = "Hello handsome hello beautiful give me some attention"; // 帅哥美女点点关注 String[] strings1 = s2.split(" ",12); // 虽然不能分割12次 但是它能够保证能分割的最大次数 不够就不分了 for (int i = 0; i < strings1.length; i++) { System.out.println(strings1[i]); } } }
特殊情况:
public class Main { public static void main(String[] args) { String s1 = "192.168.1.2"; String[] strings = s1.split("\\."); for (int i = 0; i < strings.length; i++) { System.out.println(strings[i]); } System.out.println("========="); String s2 = "C:\\APP\\Java\\jdk1.8\\bin\\java.exe"; String[] strings1 = s2.split("\\\\"); for (int i = 0; i < strings1.length; i++) { System.out.println(strings1[i]); } System.out.println("========="); String s3 = "name=zhangsan&age=18"; String[] strings2 = s3.split("&|="); for (int i = 0; i < strings2.length; i++) { System.out.println(strings2[i]); } } }
【注意事项】:
- 字符"|“,”*“,”+“都得加上转义字符,前面加上”\".
- 而如果是"“,那么就得写成”\\".
- 如果一个字符串中有多个分隔符,可以用"|"作为连字符.
多次拆分:
public class Main { public static void main(String[] args) { String s1 = "name=zhangsan&age=18"; String[] strings = s1.split("&"); for (String x:strings) { String[] strings2 = x.split("="); for (String x1 :strings2) { System.out.println(x1); } } } }
10.1.7 字符串的截取
从一个完整的字符串之中截取出部分内容。可用方法如下:
方法 | 功能 |
String substring(int beginIndex) | 从指定索引截取到结尾 |
String substring(int beginIndex, int endIndex) | 截取部分内容 |
public class Main { public static void main(String[] args) { String s1 = "helloworld" ; System.out.println(s1.substring(5)); // world System.out.println(s1.substring(0, 5)); // hello 包含 0 下标的字符, 不包含 5 下标 } }