一、继承
面向对象特点:封装、继承、多态
1.继承
1.1 为什么需要继承
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序时就需要考虑。
比如:狗和猫,它们都是一个动物。
代码实现:
java
// Dog.java
public class Dog{
String name;
int age;
float weight;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
void Bark(){
System.out.println(name + "汪汪汪~~~");
}
}
// Cat.Java
public class Cat{
String name;
int age;
float weight;
public void eat() {
System.out.println(name + "正在吃饭");
}
public void sleep() {
System.out.println(name + "正在睡觉");
}
void mew() {
System.out.println(name + "喵喵喵~~~");
}
}
通过观察上述代码会发现,猫和狗的类中存在大量重复,如下所示:

那能否将这些共性抽取呢?面向对象思想中提出了 **继承****的概念,****专门用来进行共性抽取,实现代码复用****。**
1.2 继承概念
继承(inheritance)机制 :是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能 ,这样产生新的类,称派生类 。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。【所以父类里都是共性】
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想来达到共用。

上述图示中,Dog和Cat都继承了Animal类,其中:Animal类称为父类/基类或超类,Dog和Cat可以称为Animal的子类/派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态(后序讲)。
1.3 继承的语法
在Java中如果要表示类之间的继承关系,需要借助extends关键字,具体如下:
java
修饰符 class ⼦类 extends ⽗类 {
// ...
}
对1.2中场景使用继承方式重新设计:
java
// Animal.java
public class Animal{
String name;
int age;
public void eat(){
System.out.println(name + "正在吃饭");
}
public void sleep(){
System.out.println(name + "正在睡觉");
}
}
// Dog.java
public class Dog extends Animal{
void bark(){
System.out.println(name + "汪汪汪~~~");
}
}
// Cat.Java
public class Cat extends Animal{
void mew(){
System.out.println(name + "喵喵喵~~~");
}
}
// TestExtend.java
public class TestExtend {
public static void main(String[] args) {
Dog dog = new Dog();
// dog类中并没有定义任何成员变量,name和age属性肯定是从⽗类Animal中继承下来的
System.out.println(dog.name);
System.out.println(dog.age);
// dog访问的eat()和sleep()⽅法也是从Animal中继承下来的
dog.eat();
dog.sleep();
dog.bark();
}
}
从上述代码我们可以看到Dog类和Cat类的部分代码重复使用了Animal中的代码。
注意:
- 子类会将父类中的成员变量或者成员方法继承到子类中。(所以在调用时,既可以调用子类中有的方法/变量,也可以调用子类没有的【父类有的属性,会被继承】)
- 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
1.4父类成员访问
在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
1.4.1 子类中访问父类的成员变
- 量子类和父类不存在同名成员变量
java
public class Base {
int a;
int b;
}
public class Derived extends Base{
int c;
public void method(){
a = 10; // 访问从⽗类中继承下来的a
b = 20; // 访问从⽗类中继承下来的b
c = 30; // 访问⼦类⾃⼰的c
}
}
- 子类和父类成员变量同名
java
public class Base {
int a;
int b;
int c;
}
public class Derived extends Base{
int a; // 与⽗类中成员a同名,且类型相同
char b; // 与⽗类中成员b同名,但类型不同
public void method(){
a = 100; // 访问⽗类继承的a,还是⼦类⾃⼰新增的a?
b = 101; // 访问⽗类继承的b,还是⼦类⾃⼰新增的b?
c = 102; // ⼦类没有c,访问的肯定是从⽗类继承下来的c
// d = 103; // 编译失败,因为⽗类和⼦类都没有定义成员变量b
}
}

在 **<font style="color:#DF2A3F;background-color:#FBF5CB;">Derived</font>**类重点 **<font style="color:#DF2A3F;background-color:#FBF5CB;">method</font>**方法中访问a和b访问的都是优先访问子类自己的!
如果要访问父类的,使用关键字super

在子类方法中或者通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问自己的成员变量。
- 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
1.4.2 子类中访问父类的成员方法
- 成员方法名字不同
java
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
}
public class Derived extends Base{
public void methodB(){
System.out.println("Derived中的methodB()⽅法");
}
public void methodC(){
methodB(); // 访问⼦类⾃⼰的methodB()
methodA(); // 访问⽗类继承的methodA()
// methodD(); // 编译失败,在整个继承体系中没有发现⽅法methodD()
}
}
总结:成员方法没有同名时,在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
- 成员方法名字相同
java
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
public void methodA() {
System.out.println("Derived中的method()⽅法");
}
public void methodB(){
System.out.println("Derived中的methodB()⽅法");
}
public void methodC(){
methodA(); // 优先访问⼦类中的methodA()
methodB(); // 直接访问,则永远访问到的都是⼦类中的methodB(),基类的⽆法访问到
}
}
【说明】
- 通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
- 通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;
**问题:**如果子类中存在与父类中相同的成员时,那如何在子类中访问父类相同名称的成员呢?
1.5 super关键字
由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的,Java提供了**super关键字 ,该关键字主要作用:在子类方法中访问父类的成员。**
java
public class Base {
int a;
int b;
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
int a; // 与⽗类中成员变量同名且类型相同
// 与⽗类中methodA()构成重载
public void methodA(int a) {
System.out.println("Derived中的method()⽅法");
}
// 与基类中methodB()构成重写(即原型⼀致,重写后序详细介绍)
public void methodB(){
System.out.println("Derived中的methodB()⽅法");
}
public void methodC(){
// 对于同名的成员变量,直接访问时,访问的都是⼦类的
a = 100; // 等价于: this.a = 100;
b = 101; // 等价于: this.b = 101;
// 注意:this是当前对象的引⽤
// 访问⽗类的成员变量时,需要借助super关键字
// super是获取到⼦类对象中从基类继承下来的部分
super.a = 200;
super.b = 201;
// ⽗类和⼦类中构成重载的⽅法,直接可以通过参数列表区分访问⽗类还是⼦类⽅法
methodA(); // 没有传参,访问⽗类中的methodA()
methodA(20); // 传递int参数,访问⼦类中的methodA(int)
// 如果在⼦类中要访问重写的基类⽅法,则需要借助super关键字
methodB(); // 直接访问,则永远访问到的都是⼦类中的methodA(),基类的⽆法访问到
super.methodB(); // 访问基类的methodB()
}
}
在子类方法中,如果想要明确访问父类中成员时,借助super关键字即可。
总结:
- super可以访问父类的成员变量
- super可以访问父类的成员方法
- super还可以调用父类当中指定的构造方法
注意事项
- 只能在非静态方法中使用
- 在子类方法中,访问父类的成员变量和方法。
1.6 子类构造方法
java
public class Base {
public Base(int a){ //有带一个参数的构造方法
System.out.println("Base():" + a);
}
}
public class Derived extends Base{
public Derived(){
System.out.println("Derived()");
}
}
public class Test {
public static void main(String[] args) {
Derived d = new Derived();
}
}
问题:该代码为什么会编译错误呢?
事实上,当我们在构造子类对象的时候,需要先调用基类构造方法,然后执行子类的构造方法。所以,我们需要**在子类对象构造完成之前,先帮助父类对其中的成员进行初始化**。具体方式如下:
java
public class Base {
public Base(int a){
System.out.println("Base():" + a);
}
}
public class Derived extends Base{
public Derived(){
super(10);//此时通过super(参数)的形式
System.out.println("Derived()");
}
}
public class Test {
public static void main(String[] args) {
Derived d = new Derived();
}
}
//打印结果:
Base():10
Derived()
【注意事项】
- 通过super(参数)的形式可以调用父类指定的构造方法
- super()的形式只能出现在子类的构造方法当中且****必须在第一行
问题:如下代码为什么没有报错呢?
java
public class Base {
public Base(){
System.out.println("Base()");
}
}
public class Derived extends Base{
public Derived(){
// super(); // 注意⼦类构造⽅法中默认会调⽤基类的⽆参构造⽅法:super(),
// ⽤⼾没有写时,默认会添加⼀个super(),⽽且super()必须是⼦类构造⽅法中第⼀条语句,
// 并且只能出现⼀次
System.out.println("Derived()");
}
}
public class Test {
public static void main(String[] args) {
Derived d = new Derived();
}
}
结果打印:
Base()
Derived()
【注意事项】
- 正如注释处所说,当父类的构造方法是不带参数的构造方法且只有这一个的情况下,默认会添加一个super()。
【总结】
- 在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,因为:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分。父子父子肯定是先有父再有子,所以在构造子类对象时候,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整。
- 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败
- 在子类构造方法中,super(...)调用父类构造时,必须是子类构造函数中第一条语句
- super(...)只能在子类构造方法中出现一次,并且不能和this同时出现
1.7 super和this
super和this都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的
第一条语句,那他们之间有什么区别呢?
【相同点】
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
【不同点】
- this是当前对象的引用,当前对象即调用实例方法的对象,Super相当于是子类对象中从父类继承下来部分成员的引用

- 在非静态成员方法中,this用来访问本类的方法和属性,Super用来访问父类继承下来的方法和属性
- 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造方法中出现
- 构造方法中一定会存在super()的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有
1.8 继承关系上代码块等的初始化顺序

1.8.1 回顾之前的初始化顺序
简单回顾一下几个重要的代码块:实例代码块和静态代码块。在没有继承关系时的执行顺序。
java
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("构造⽅法执⾏");
}
{
System.out.println("实例代码块执⾏");
}
static {
System.out.println("静态代码块执⾏");
}
}
public class TestDemo {
public static void main(String[] args) {
Person person1 = new Person("bit",10);
System.out.println("============================");
Person person2 = new Person("gaobo",20);
}
}
执行结果:
java
静态代码块执⾏
实例代码块执⾏
构造⽅法执⾏
============================
实例代码块执⾏
构造⽅法执⾏
- 静态代码块先执行,并且只执行一次,在类加载阶段执行
- 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
1.8.2 继承关系上的执行顺序
java
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person:构造⽅法执⾏");
}
{
System.out.println("Person:实例代码块执⾏");
}
static {
System.out.println("Person:静态代码块执⾏");
}
}
class Student extends Person{
public Student(String name,int age) {
super(name,age);
System.out.println("Student:构造⽅法执⾏");
}
{
System.out.println("Student:实例代码块执⾏");
}
static {
System.out.println("Student:静态代码块执⾏");
}
}
public class TestDemo4 {
public static void main(String[] args) {
Student student1 = new Student("张三",19);
System.out.println("===========================");
Student student2 = new Student("gaobo",20);
}
}
执行结果:
java
Person:静态代码块执⾏
Student:静态代码块执⾏
Person:实例代码块执⾏
Person:构造⽅法执⾏
Student:实例代码块执⾏
Student:构造⽅法执⾏
===========================
Person:实例代码块执⾏
Person:构造⽅法执⾏
Student:实例代码块执⾏
Student:构造⽅法执⾏
通过分析执行结果,得出以下结论:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行
- 父类实例代码块和父类构造方法紧接着执行
- 子类的实例代码块和子类构造方法紧接着再执行
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
1.9 访问修饰限定符-protected关键字
在类和对象章节中,为了实现封装特性,Java中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。

那父类中不同访问权限的成员,在子类中的可见性又是什么样子的呢?
1.9.1 private关键字
如果被private关键字修饰该类当中成员变量,成员方法等只能在当前类当中使用
1.9.2 不加任何的关键字
如果不加任何的关键字修饰该类当中成员变量,成员方法等表示默认权限,默认权限代表的也是包访问权限,意味着只能在当前包当中使用
1.9.3 protected关键字
如果被protected关键字修饰该类当中成员变量,成员方法等表示要么只能在同一个包中的类中进行访问,要么在不同包中只能通过在继承关系上的子类对象来访问。
示例代码如下:
java
package com.bit.demo1;
public class Base {
protected int b = 100; //受保护的b
}
java
package com.bit.demo2;
import com.bit.demo1.Base;
public class Derived extends Base {
public static void main(String[] args) {
Derived derived = new Derived (); //实例化后
System.out.println(derived.b); //进行访问
}
}
两种方法:

反例:
java
package com.bit.demo2;
import com.bit.demo1.Base;
class C extends Base {
public static void main(String[] args) {
C c = new C();
System.out.println(c.b);
}
}
public class Derived extends Base {
public static void main(String[] args) {
Derived derived = new Derived ();
System.out.println(derived.b);
C c = new C();
System.out.println(c.b);//该处出现错误
}
}
虽然 Derived 类继承自Base类,但它不能访问其他 Base 类子类(这里是C 类) 实例的protected 成员。
【通俗易懂的讲】
就是a是父类,有两个b,c子类,我可以在b类中通过实例化或者super来访问父类a中的成员变量,c类同样。但是!在b类中不能通过实例化c来访问!虽然c也是a的子类,但是一个类只能是它本身来进行父类的访问。
1.9.4 public 关键字
如果被public关键字修饰该类当中成员变量,成员方法等表示该成员变量和成员方法是公开的。
什么时候下用哪一种呢?
我们希望类要尽量做到"封装",即隐藏内部实现细节,只暴露出必要的信息给类的调用者.
因此我们在使用的时候应该尽可能的使用比较严格的访问权限.例如如果一个方法能用private,就尽量不要用public.
另外,还有一种简单粗暴的做法:将所有的字段设为 private,将所有的方法设为 public.不过这种方式属于是对访问权限的滥用,所以写代码的时候认真思考,该类提供的字段方法到底给"谁"使用(是类内部自己用,还是类的调用者使用,还是子类使用).
1.10 继承方式
在现实生活中,事物之间的关系是非常复杂,灵活多样,比如:

但Java中只支持以下几种继承方法

注意:Java中不支持多继承。(C++支持)
时刻牢记,我们写的类是现实事物的抽象.而我们真正在公司中所遇到的项目往往业务比较复杂,可能会涉及到一系列复杂的概念,都需要我们使用代码来表示,所以我们真实项目中所写的类也会有很多。类之间的关系也会更加复杂,
但是即使如此,我们并不希望类之间的继承层次太复杂。**一般我们不希望出现超过三层的继承关系.**如果继承层次太多,就需要考虑对代码进行重构了.
如果想从语法上进行限制继承,就可以使用final关键字
1.10.1 final 关键字
final关键可以用来修饰变量、成员方法以及类。
- 修饰变量或字段,表示常量(即不能修改)
java
final int a = 10;
a = 20; // 编译出错
- 修饰类:表示此类不能被继承
java
final public class Animal {
...
}
public class Bird extends Animal {
...
}
我们平时是用的String字符串类,就是用final修饰的,不能被继承.

- 修饰方法:表示该方法不能被重写(后序介绍)
1.11 继承与组合
和继承类似,组合也是一种表达类之间关系的方式,也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如extends这样的关键字),仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车
汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的。
java
// 轮胎类
class Tire{
// ...
}
// 发动机类
class Engine{
// ...
}
// ⻋载系统类
class VehicleSystem{
// ...
}
class Car{
private Tire tire; // 可以复⽤轮胎中的属性和⽅法
private Engine engine; // 可以复⽤发动机中的属性和⽅法
private VehicleSystem vs; // 可以复⽤⻋载系统中的属性和⽅法
// ...
}
// 奔驰是汽⻋
class Benz extend Car{
// 将汽⻋中包含的:轮胎、发送机、⻋载系统全部继承下来
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。
组合和继承的相关知识整理: https://www.hollischuang.com/archives/1319
2.小试牛刀
3.关于Java中的继承,以下哪个陈述是正确的?
A.一个类可以同时继承多个类
B. 子类可以直接访问父类的私有(private)成员
C. 使用 final 关键字修饰的类可以被继承
D. 子类可以重写(override)父类的方法
4.以下关于Java继承的说法,哪一个是错误的?
A.子类可以通过 super 关键字调用父类的构造方法
B.所有的Java类都直接或间接继承自java.lang.Object 类
C.构造方法可以被子类继承
D. 使用 protected 修饰的成员可以被子类访问答案解析:
答案:3. D 4. C
Java当中的类,如果没有显示指明继承了哪个类,一般默认都继承自Object
Object类-----一般祖先类
二、多态
1.多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
总的来说:同一件事情,发生在不同对象身上,就会产生不同的结果。

1.1 多态实现条件
在Java中要实现多态,必须要满足如下几个条件,缺一不可:
1.必须在继承体系下
2.子类必须要对父类中方法进行重写
3.通过父类的引用调用重写的方法
多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。
java
public class Animal {
String name;
int age;
public Animal(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println(name + "吃饭");
}
}
public class Cat extends Animal{
public Cat(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃⻥~~~");
}
}
public class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃⻣头~~~");
}
}
///////////////////////////////分割线//////////////////////////////////////////////
public class TestAnimal {
// 编译器在编译代码时,并不知道要调⽤Dog 还是 Cat 中eat的⽅法
// 等程序运⾏起来后,形参a引⽤的具体对象确定后,才知道调⽤那个⽅法
// 注意:此处的形参类型必须时⽗类类型才可以
public static void eat(Animal a){
a.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("⼩七", 1);
eat(cat);
eat(dog);
}
}
运⾏结果:
元宝吃⻥~~~
元宝正在睡觉
⼩七吃⻣头~~~
⼩七正在睡觉
在上述代码中,分割线上方的代码是类的实现者 编写的,分割线下方的代码是类的调用者编写的.
当类的调用者在编写eat这个方法的时候,参数类型为Animal(父类),此时在该方法内部并不知道 ,也不关注 当前的a引用指向的是哪个类型(哪个子类)的实例.此时a这个引用调用eat方法可能会有多种不同的表现(和a引l用的实例相关),这种行为就称为多态.
1.2 重写
区别于:(面试会问方法重写和重载的区别)

重写(override):也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写,**返回值和形参都不能改变。即外壳不变,核心重写!**重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
【方法重写的规则】
- 子类在重写父类的方法时,一般必须与父类方法原型一致:返回值类型方法名(参数列表)要完全一致
- **被重写的方法返回值类型可以不同,但是必须是具有父子关系的(协变类型)**如下图:

- 访问权限不能比父类中被重写的方法的访问权限更低 。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为protected【子类的访问权限>=父类的访问权限】

- 父类被static、private、final修饰的方法、构造方法都不能被重写。(会报错)


- 重写的方法,可以使用
**<font style="color:#DF2A3F;background-color:#FBF5CB;">@Override</font>**注解来显式指定.有了这个注解能帮我们进行一些合法性校验.例如不小心将方法名字拼写错了(比如写成aet),那么此时编译器就会发现父类中没有aet方法,就会编译报错,提示无法构成重写.
**【重写和重载的区别】 **

即:方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
【重写的设计原则】
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来老的类上进行修改,因为原来的类,可能还在有用户使用 ,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
- **静态绑定:**也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
- **动态绑定:**也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
1.3 向上转型和向下转型
1.3.1 向上转型
【父类引用只能调用父类自己的方法】

向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名= new 子类类型()
java
Animal animal = new Cat("元宝",2); //父类引用 引用子类对象
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换
【使用场景】
1.直接赋值
2.方法传参
3.方法返回
java
public class TestAnimal {
// 2. ⽅法传参:形参为⽗类型引⽤,可以接收任意⼦类的对象
public static void eatFood(Animal a){
a.eat();
}
// 3. 作返回值:返回任意⼦类对象
public static Animal buyAnimal(String var){
if("狗".equals(var) ){
return new Dog("狗狗",1);
}else if("猫" .equals(var)){
return new Cat("猫猫", 1);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("元宝",2); // 1. 直接赋值:⼦类对象赋值给⽗类对象
Dog dog = new Dog("⼩七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
向上转型的优点:让代码实现更简单灵活。
向上转型的缺陷:不能调用到子类特有的方法。
1.3.2 向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。

java
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("⼩七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
// 编译失败,编译时编译器将animal当成Animal对象处理
// ⽽Animal类中没有bark⽅法,因此编译失败
// animal.bark();
// 向上转型
// 程序可以通过编程,但运⾏时抛出异常---因为:animal实际指向的是狗
// 现在要强制还原为猫,⽆法正常还原,运⾏时抛出:ClassCastException
cat = (Cat)animal; //强转
cat.mew();
// animal本来指向的就是狗,因此将animal还原为狗也是安全的
dog = (Dog)animal;
dog.bark();
}
}

- 向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了
**<font style="color:#DF2A3F;">instanceof</font>**,如果该表达式为true,则可以安全转换。
java
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("⼩七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
if(animal instanceof Cat){
cat = (Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog = (Dog)animal;
dog.bark();
}
}
}
instanceof关键词官方介绍:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.20.2
1.4 多态的优缺点
java
class Shape {
//属性....
public void draw() {
System.out.println("画图形!");
}
}
class Rect extends Shape{
@Override
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("❀");
}
}
【使用多态的好处】
1.能够降低代码的"圈复杂度",避免使用大量的if-else
1.什么叫"圈复杂度"?
2.圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解.而如果有很多的条件分支或者循环语句,就认为理解起来更复杂。
3.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数,这个个数就称为"圈复杂度"如果一个方法的圈复杂度太高,就需要考虑重构.
4.不同公司对于代码的圈复杂度的规范不一样。一般不会超过10.
例如我们现在需要打印的不是一个形状了,而是多个形状.如果不基于多态,实现代码如下:
java
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
使用多态,代码更简单
java
public static void drawShapes() {
// 我们创建了⼀个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
【代码注释】
"匿名对象"

2.可扩展能力更强
如果要新增一种新的形状,使用多态的方式代码改动成本也比较低.
java
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法),只要创建一个新类的实例就可以了,改动成本很低.
而对于不用多态的情况,就要把drawShapes中的if-else进行一定的修改,改动成本更高.
1.5 避免在构造方法中调用重写的方法
一段有坑的代码.我们创建两个类,B是父类,D是子类.D中重写func方法.并且在B的构造方法中调用 func
java
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
// 执⾏结果
D.func() 0
- 构造 D 对象的同时,会调用B 的构造方法.
- B 的构造方法中调用了 func 方法,此时会触发动态绑定, 会调用到 D 中的 func
- 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态,值为 0.
- 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
结论:"用尽量简单的方式使对象进入可工作状态"尽量不要在构造器中调用方法(如果这个方法被子类重写,就会触发动态绑定,但是此时子类对象还没构造完成),可能会出现一些隐藏的但是又极难发现的问题.
2.小试牛刀
1.以下关于Java多态的描述,哪一个是错误的?
A.多态允许不同类的对象对同一消息做出响应
B.方法重载是编译时多态的一种形式
C.子类可以重写父类的final方法来实现运行时多态
D.接口是实现多态的一种方式
- 下列哪种情况下会发生向上转型(upcasting)?
A.将子类对象赋值给父类引用
B. 将父类对象赋值给子类引用
C.在子类中调用super()
D.使用instanceof运算符
3.关于方法重写(override),以下哪个陈述是正确的?
A.重写方法的访问修饰符必须与被重写方法完全相同
B.重写方法可以抛出比被重写方法更广泛的检查异常
C.静态方法可以被重写
D.重写方法的返回类型可以是被重写方法返回类型的子类
4.在Java中,以下哪种情况下不会发生多态行为?
A.使用接口引用调用实现类的方法
B. 使用父类引用调用被子类重写的方法
C. 调用重载的方法
D. 使用final 关键字修饰的方法
答案解析:1.C 2.A 3.D 4.D
三、抽象类和接口
1. 抽象类
1.1 抽象类概念
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个**++具体的对象++****(或者是不能描述一个具体的对象的时候),这样的类就是抽象类** 。抽象类需使用 **<font style="color:#DF2A3F;">abstract</font>**关键字进行修饰


比如:在打印图形例子中,我们发现,父类Shape中的draw方法好像并没有什么实际工作,主要的绘制图形都是由Shape的各种子类的draw方法来完成的.像这种没有实际工作的方法,我们可以把它设计成一个抽象方法(abstract method) ,包含抽象方法的类我们称为抽象类(abstract class).
抽象方法****:从语法上来说就是这个方法被 **<font style="color:rgb(6, 10, 38);background-color:#FBF5CB;">abstract</font>**修饰了,修饰之后可以没有具体的方法体
1.2 抽象类语法
在Java中,一个类如果被<font style="color:rgb(6, 10, 38);">abstract</font>修饰称为抽象类,抽象类中被<font style="color:#DF2A3F;">abstract</font>修饰的方法称为抽象方法,抽象方法不用给出具体的实现体。
java
//抽象类:被abstract修饰的类
public abstract class Shape {
//抽象方法:被abstract修饰的方法,没有方法体
abstract public void draw();
abstract void calcArea();
//抽象类也是类,也可以增加普通方法和属性
public double getArea() {
return area;
}
protected double area; //面积
}
注意:抽象类也是类,内部可以包含普通方法和属性,甚至构造方法
抽象类的特点:
1. <font style="color:#DF2A3F;">抽象类当中可以定义和普通类一样的成员变量和成员方法</font>
2. <font style="color:#DF2A3F;">抽象类中不一定必须得有抽象方法,没有抽象方法这个类也是可以定义的</font>
3. <font style="color:#DF2A3F;background-color:#FBF5CB;">抽象类</font>**<font style="color:#DF2A3F;background-color:#FBF5CB;">不能实例化</font>**<font style="color:#DF2A3F;background-color:#FBF5CB;">【可以被继承】</font>
4. <font style="color:#DF2A3F;">当一个普通类 继承抽象类之后,这个普通类需要重写父类抽象类当中的抽象方法</font>



5. <font style="color:#DF2A3F;">抽象类当中可以有构造方法,其作用就是为了子类当中调用,来初始化父类当中的成员
本身如果不是抽象方法,在继承中父类有构造方法,并且有参数,子类必须先要调用父类的构造方法来初始化父类成员,先帮父类进行初始化。但是现在子类继承抽象类,抽象类不能实例化,所以子类继承父类抽象类就是为了重写,给父类进行实例化

**<font style="color:#117CEE;">alt+enter</font>**
6. <font style="color:#DF2A3F;">一个抽象类当中 可以没有抽象方法,但如果有一个类有抽象方法,那么这个类一定是抽象类</font>
7. <font style="color:#DF2A3F;">抽象类 也是可以发生动态绑定,向上转型,多态等特性的</font>
8. <font style="color:#DF2A3F;">如果一个抽象类A 继承了 抽象类B,那么在抽象类A中可以不重写抽象类B中的方法,但是当一个普通类C继承了这个抽象类A,此时在普通类C中,不仅要重写抽象类B中的方法,还要重写抽象类A的方法

- private,static,final不能修饰抽象方法(因为子类要继承父类抽象方法)
【因为抽象类不能实例化,所以通过下面图片里的方法来调用父类中的方法(通过调用子类对象,子类继承父类抽象类,为此调用的父类的抽象方法)】

1.3 抽象类特性
- 抽象类不能直接实例化对象(因为抽象类不能表示一个具体的类)
java
Shape shape = new Shape();//编译出错
Error:(30, 23) java: Shape是抽象的;无法实例化
- 抽象方法不能是private的
java
abstract class Shape {
abstract private void draw();
}
//编译出错
Error:(4, 27) java:非法的修饰符组合: abstract和private
- 抽象方法不能被final和static修饰,因为抽象方法要被子类重写
java
public abstract class Shape {
abstract final void methodA();
abstract public static void methodB();
}
//编译报错:
// Error:(20, 25) java:非法的修饰符组合: abstract和final
// Error:(21, 33) java:非法的修饰符组合: abstract和static
- 抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用
<font style="color:rgb(6, 10, 38);">abstract</font>修饰
java
//矩形类
public class Rect extends Shape {
private double length;
private double width;
Rect(double length, double width) {
this.length = length;
this.width = width;
}
public void draw() {
System.out.println("矩形: length=" + length + " width=" + width);
}
public void calcArea() {
area = length * width;
}
}
//圆类:
public class Circle extends Shape {
private double r;
final private static double PI = 3.14;
public Circle(double r) {
this.r = r;
}
public void draw() {
System.out.println("圆:r=" + r);
}
public void calcArea() {
area = PI * r * r;
}
}
//三角形类:
public abstract class Triangle extends Shape {
private double a;
private double b;
private double c;
@Override
public void draw() {
System.out.println("三角形:a=" + a + " b=" + b + " c=" + c);
}
//三角形:直角三角形、等腰三角形等,还可以继续细化
//@Override
//double calcArea();
//编译失败:要么实现该抽象方法,要么将三角形设计为抽象类
}
- 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
- 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量
1.4 抽象类的作用
抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法.
提问:普通的类也可以被继承,普通的方法也可以被重写呀,为啥非得用抽象类和抽象方法呢?
答:确实如此.但是使用抽象类相当于多了一重编译器的校验.
使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成.那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的.但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题.
很多语法存在的意义都是为了"预防出错",例如我们曾经用过的final也是类似,创建的变量用户不去修改,就相当于常量。但是加上final能够在不小心误修改的时候,让编译器及时提醒我们充分利用编译器的校验,在实际开发中是非常有意义的.
2. 接口
创建接口


通常情况下,命名也可能是形容词...able等
2.1 接口的概念
在现实生活中,接口的例子比比皆是,比如:笔记本上的USB口,电源插座等
- 电脑的USB口上,可以插:U盘、鼠标、键盘...所有符合USB协议的设备
- 电源插座插孔上,可以插:电脑、电视机、电饭煲...所有符合规范的设备
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
2.2 语法规则
接口的定义格式与定义类的格式基本相同,将class关键字换成 **<font style="color:#DF2A3F;background-color:#FBF5CB;">interface</font>**关键字,就定义了一个接口。
java
public interface 接口名称 {
//抽象方法
public abstract void method1(); // public abstract是固定搭配,可以不写
public void method2();
abstract void method3();
void method4();
//注意:在接口中上述写法都是抽象方法,跟推荐方式4,代码更简洁
}
提示:【软要求,不做硬性规定】
- 创建接口时,接口的命名一般以大写字母I开头.
- 接口的命名一般使用"形容词"词性的单词.
- 阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号,保持代码的简洁性
例如:
不用加默认内容
关于接口
- 被关键字
**<font style="color:#DF2A3F;">interface</font>**修饰的 - 接口当中的成员变量默认是私有的(public static final)【常量】
- 接口当中的抽象方法 默认是
**<font style="color:#DF2A3F;">public abstract</font>**修饰的
所以可以写可以不写 - 接口当中的方法 不能有具体的实现,但是:如果这个方法被
**<font style="color:#DF2A3F;">static</font>**或者**<font style="color:#DF2A3F;">default</font>**修饰之后,可以有具体实现
不同版本JDK特性:https://www.cnblogs.com/hanease/p/19143544 - 接口也不可以实例化
- 一个类可以通过关键字
**<font style="color:#DF2A3F;">implements</font>**实现一个或者多个接口


- 当一个普通类实现该接口以后,要重写这个接口里的所有抽象方法
如果是抽象类,可以不用写,不会报错,因为抽象类本身就不用去实现抽象方法

- 可以发生向上转型、动态绑定、多态
- 因为接口当中的方法默认是
<font style="color:#DF2A3F;">public</font>修饰的,所以子类当中重写的时候权限只能是<font style="color:#DF2A3F;">public</font>
2.3 接口使用
接口不能直接使用,****必须要有一个"实现类"来"实现"该接口,实现接口中的所有抽象方法。
java
public class 类名称 implements 接口名称 {
//...
}
注意:子类和父类之间是extends继承关系,类与接口之间是implements实现关系。
实现笔记本电脑使用USB鼠标、USB键盘的例子
- USB接口:包含打开设备、关闭设备功能
- 笔记本类:包含开机功能、关机功能、使用USB设备功能
- 鼠标类:实现USB接口,并具备点击功能
- 键盘类:实现USB接口,并具备输入功能
java
// USB接口
public interface USB {
void openDevice();
void closeDevice();
}
//鼠标类,实现USB接口
public class Mouse implements USB {
@Override
public void openDevice() {
System.out.println("打开鼠标");
}
@Override
public void closeDevice() {
System.out.println("关闭鼠标");
}
public void click() {
System.out.println("鼠标点击");
}
}
//键盘类,实现USB接口
public class KeyBoard implements USB {
@Override
public void openDevice() {
System.out.println("打开键盘");
}
@Override
public void closeDevice() {
System.out.println("关闭键盘");
}
public void inPut() {
System.out.println("键盘输入");
}
}
//笔记本类:使用USB设备
public class Computer {
public void powerOn() {
System.out.println("打开笔记本电脑");
}
public void powerOff() {
System.out.println("关闭笔记本电脑");
}
public void useDevice(USB usb) {
usb.openDevice();
if (usb instanceof Mouse) {
Mouse mouse = (Mouse) usb;
mouse.click();
} else if (usb instanceof KeyBoard) {
KeyBoard keyBoard = (KeyBoard) usb;
keyBoard.inPut();
}
usb.closeDevice();
}
}
//测试类:
public class TestUSB {
public static void main(String[] args) {
Computer computer = new Computer();
computer.powerOn();
//使用鼠标设备
computer.useDevice(new Mouse());
//使用键盘设备
computer.useDevice(new KeyBoard());
computer.powerOff();
}
}
2.4 接口特性
- 接口类型是一种引用类型 ,但是不能直接new接口的对象

java
public class TestUSB {
public static void main(String[] args) {
USB usb = new USB();
}
}
// Error:(10, 19) java: day20210915.USB是抽象的;无法实例化
- 接口中每一个方法都是public的抽象方法,即接口中的方法会被隐式的指定为public abstract(只能是public abstract,其他修饰符都会报错)
java
public interface USB {
// Error:(4, 18) java:此处不允许使用修饰符private
private void openDevice();
void closeDevice();
}
- 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现
java
public interface USB {
void openDevice();
//编译失败:因为接口中的方式默认为抽象方法
// Error:(5, 23) java:接口抽象方法不能带有主体
void closeDevice() {
System.out.println("关闭USB设备");
}
}
- 重写接口中方法时,不能使用默认的访问权限
java
public interface USB {
void openDevice(); //默认是public的
void closeDevice(); //默认是public的
}
public class Mouse implements USB {
@Override
void openDevice() {
System.out.println("打开鼠标");
}
//...
}
//编译报错,重写USB中openDevice方法时,不能使用默认修饰符
//正在尝试分配更低的访问权限;以前为public
- 接口中可以含有变量,但是接口中的变量会被隐式的指定为public static final变量
java
public interface USB {
double brand = 3.0; //默认被:final public static修饰
void openDevice();
void closeDevice();
}
public class TestUSB {
public static void main(String[] args) {
System.out.println(USB.brand); //可以直接通过接口名访问,说明是静态的
//编译报错:Error:(12, 12) java:无法为最终变量brand分配值
USB.brand = 2.0; //说明brand具有final属性
}
}
- 接口中不能有静态代码块和构造方法
java
public interface USB {
//编译失败
public USB() {}
{} //编译失败
void openDevice();
void closeDevice();
}
- 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
- 如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
- jdk8中:接口中还可以包含default方法。
抽象类和接口的区别:
抽象类当中可以定义构造方法,和普通类唯一的区别就是多了个抽象方法。接口中不能定义和普通类一样的成员变量,只能是<font style="color:#DF2A3F;">public static final</font>变量,也不能有具体实现的普通方法,只能是抽象方法或者被<font style="color:#DF2A3F;">static</font>/<font style="color:#DF2A3F;">default</font>修饰之后,可以有具体实现。
共同点:不能被实例化
**2.5 实现多个接口
**
Java中,一个类可以继承一个类的同时,还可以实现接口,并且可以实现多个接口
在继承中,不可以多继承,但可以实现多个接口,来实现多继承这种情况

在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承 ,但是一个类可以实现多个接口。
下面通过类来表示一组动物.
java
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口,分别表示"会飞的","会跑的","会游泳的".
java
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
接下来我们创建几个具体的动物:
猫,是会跑的.
java
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼,是会游的.
java
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
青蛙,既能跑,又能游(两栖动物)
java
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
注意:****一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类。
提示,IDEA中使用ctrl+i快速实现接口
还有一种神奇的动物,水陆空三栖,叫做"鸭子"
java
class Duck extends Animal implements IRunning, ISwimming, IFlying {
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特性.
- 猫是一种动物,具有会跑的特性.
- 青蛙也是一种动物,既能跑,也能游泳
- 鸭子也是一种动物,既能跑,也能游,还能飞
这样设计有什么好处呢?时刻牢记多态的好处,让程序猿忘记类型.有了接口之后,类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力.
例如,现在实现一个方法,叫"散步"
java
public static void walk(IRunning running) {
System.out.println("我带着伙伴去散步");
running.run();
}
在这个walk方法内部,我们并不关注到底是哪种动物,只要参数是会跑的,就行
java
Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
//执行结果
我带着伙伴去散步
小猫正在用四条腿跑
我带着伙伴去散步
小青蛙正在往前跳
甚至参数可以不是"动物",只要会跑!
java
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
Robot robot = new Robot("机器人");
walk(robot);
//执行结果
机器人正在用轮子跑
2.6 接口间的继承
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。接口可以继承一个接口,达到复用的效果.使用 **<font style="color:#DF2A3F;">extends</font>**关键字.
java
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
//两栖的动物,既能跑,也能游
interface IAmphibious extends IRunning, ISwimming {}
class Frog implements IAmphibious {
...
}
通过接口继承创建一个新的接口IAmphibious表示"两栖的".此时实现接口创建的Frog类,就继续要实现run方法,也需要实现swim方法.
接口间的继承相当于把多个接口合并在一起.
2.7 接口使用实例
以后自定义的类型,建议实现比较接口、toString()、hashcode、equals
下面两种比较方式是不矛盾的,可以共存的,若想使用Comparable接口 ,可以直接用,但还想比较其他特性可以继续建相对应的类使用Comparator接口 ,如NameComparator,AgeComparator,具体实现:
<要比较的类>(<>跟在接口后面,如果是Comparable接口,同样)。实现Comparator接口后进行重写,其次在要比较的类里进行实例化,最后可比较/排序,如:
对象之间进行大小关系比较
java
class Student {
public String name;
public int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
java
public class Test {
public static void main(String[] args) {
Student s1 = new Student("zhangsan",10);
Student s2 = new Student("lisi",20);
System.out.println(s1 > s2);
}
此时程序会编译报错,并没有指定根据分数还是什么进行大小比较。所以,应该指定以什么样的方式进行比较。
不太好的点是,只要按照该方式比较后,不灵活。无法按照其他方式进行比较
方式一:使用Comparable接口****【不能轻易修改接口里的比较方式/规则】
让我们的Student类实现**<font style="color:rgb(6, 10, 38);">Comparable</font>**接口,并实现其中的compareTo方法
java
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student) o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
java
public class Test {
public static void main(String[] args) {
Student s1 = new Student("zhangsan",10);
Student s2 = new Student("lisi",20);
System.out.println(s1.compareTo(s2));
}
}
如果s1大于s2那么返回大于0的数字,如果相同返回0,否则返回小于0的数字。
方式二:使用Comparator接口

java
class Student {
public String name;
public int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
//分数比较
class ScoreComparator implements Comparator<Student> {
@Override //进行重写生成
public int compare(Student o1, Student o2) {
return o1.score - o2.score;
}
}
//名字比较(字符串)
class NameComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
java
public class Test {
public static void main(String[] args) {
Student s1 = new Student("zhangsan",10);
Student s2 = new Student("lisi",20);
//根据分数进行比较
ScoreComparator scoreComparator = new ScoreComparator();
System.out.println(scoreComparator.compare(s1, s2));
//根据姓名进行比较
NameComparator nameComparator = new NameComparator();
System.out.println(nameComparator.compare(s1, s2));
}
}

2.8 Clonable 接口和深拷贝

Java中内置了一些很有用的接口,Clonable就是其中之一. Object类中存在一个clone方法,调用这个方法可以创建一个对象的"拷贝".但是要想合法调用clone方法,必须要先实现Clonable接口,否则就会抛出CloneNotSupportedException异常.
java
class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
Animal o = null;
try {
o = (Animal) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
//输出结果
// false
浅拷贝VS深拷贝

Cloneable拷贝出的对象是一份"浅拷贝"
观察以下代码:
java
class Money {
public double m = 99.99;
}
class Person implements Cloneable {
public Money money = new Money();
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class TestDemo3 {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person();
Person person2 = (Person) person1.clone();
System.out.println("通过person2修改前的结果");
System.out.println(person1.money.m);
System.out.println(person2.money.m);
person2.money.m = 13.6;
System.out.println("通过person2修改后的结果");
System.out.println(person1.money.m);
System.out.println(person2.money.m);
}
}
//执行结果
通过person2修改前的结果
99.99
99.99
通过person2修改后的结果
13.6
13.6
如上代码,我们可以看到,通过clone,我们只是拷贝了Person对象。但是Person对象中的Money对象,并没有拷贝。通过person2这个引用修改了m的值后,person1这个引用访问m的时候,值也发生了改变。这里就是发生了浅拷贝。
实现深拷贝

2.9 抽象类和接口的区别
抽象类和接口都是Java中多态的常见使用方式.都需要重点掌握.同时又要认清两者的区别(重要!!!常见面试题).
**核心区别:**抽象类中可以包含普通方法和普通字段,这样的普通方法和字段可以被子类直接使用(不必重写),而接口中不能包含普通方法,子类必须重写所有的抽象方法.
如之前写的Animal例子.此处的Animal中包含一个name这样的属性,这个属性在任何子类中都是存在的.因此此处的Animal只能作为一个抽象类,而不应该成为一个接口.
java
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
再次提醒:
抽象类存在的意义是为了让编译器更好的校验,像Animal这样的类我们并不会直接使用,而是使用它的子类.万一不小心创建了Animal的实例,编译器会及时提醒我们.

3. 内部类
当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么这个内部的完整结构最好使用内部类。【比如火车,把每一节车厢当作内部类,也就是只为这个火车这个外部提供服务。】在Java中,可以将一个类定义在另一个类或者一个方法的内部,前者称为内部类,后者称为外部类。内部类也是封装的一种体现。

java
public class OutClass {
class InnerClass {}
}
// OutClass是外部类
// InnerClass是内部类
【注意事项】
- 定义在class类名{}花括号外部的,即使是在一个文件里,都不能称为内部类
java
public class A {}
class B {}
// A和B是两个独立的类,彼此之前没有关系
- 内部类和外部类共用同一个java源文件,但是经过编译之后,内部类会形成单独的字节码文件
3.1 内部类的分类
java
public class OutClass {
//成员位置定义:未被static修饰--->实例内部类
public class InnerClass1 {
}
//成员位置定义:被static修饰--->静态内部类
static class InnerClass2 {}
public void method() {
//方法中也可以定义内部类--->局部内部类:几乎不用
class InnerClass5 {}
}
}
根据内部类定义的位置不同,一般可以分为以下几种形式:
- 成员内部类(普通内部类:未被static修饰的成员内部类和静态内部类:被static修饰的成员内部类)
- 局部内部类(不谈修饰符)、匿名内部类
注意:内部类其实日常开发中使用并不是非常多,大家在看一些库中的代码时候可能会遇到的比较多,日常开始中使用最多的是匿名内部类。
3.2 静态内部类
被static修饰的内部成员类称为静态内部类

java
public class OutClass {
public int a;
public static int b;
//静态内部类:被static修饰的成员内部类
static class InnerClass {
public void methodInner() {
//在内部类中只能访问外部类的静态成员
// a = 100; //编译失败,因为a不是类成员变量
b = 200;
}
}
public static void main(String[] args) {
//静态内部类对象创建&成员访问
OutClass.InnerClass innerClass = new OutClass.InnerClass();
innerClass.methodInner();
}
}
【注意事项1】
- 在静态内部类中只能访问外部类中的静态成员
- 创建静态内部类对象时,不需要先创建外部类对象
【另外】

3.3 实例内部类
即未被static修饰的成员内部类。

java
public class OutClass {
public int a;
public static int b;
public int c; //与内部类重名
// 实例内部类:未被static修饰
class InnerClass {
int c;
public void methodInner() {
//在实例内部类中可以直接访问外部类中:任意访问限定符修饰的成员
a = 100;
b = 200;
//如果外部类和实例内部类中具有相同名称成员时,优先访问的是内部类自己的
c = 300;
System.out.println(c);
//如果要访问外部类同名成员时候,必须:外部类名称.this.同名成员名字
OutClass.this.c = 400;
System.out.println(OutClass.this.c); //被指定为外部类的this
}
}
public static void main(String[] args) {
//外部类:对象创建以及成员访问
OutClass outClass = new OutClass();
System.out.println(outClass.a);
System.out.println(OutClass.b);
System.out.println(outClass.c);
System.out.println("=============实例内部类的访问=============");
//要访问实例内部类中成员,必须要创建实例内部类的对象
//而普通内部类定义与外部类成员定义位置相同,因此创建实例内部类对象时必须借助外部类
//创建实例内部类对象(展开写法)
OutClass.InnerClass innerClass1 = new OutClass().new InnerClass();
//上述语法比较怪异,也可以先将外部类对象先创建出来,然后再创建实例内部类对象
OutClass.InnerClass innerClass2 = outClass.new InnerClass();
innerClass2.methodInner();
}
}
【注意事项】
- 外部类中的任何成员都可以在实例内部类方法中直接访问
- 实例内部类所处的位置与外部类成员位置相同,因此也受public、private等访问限定符的约束
- 在实例内部类方法中访问同名的成员时,****优先访问自己的,如果要访问外部类同名的成员,必须:外部类名称.this.同名成员来访问
- 实例内部类对象必须在先有外部类对象前提下才能创建
- 实例内部类的非静态方法中包含了一个指向外部类对象的引用
- 外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象。
3.4 局部内部类
定义在外部类的方法体或者{}中,该种内部类只能在其定义的位置使用,一般使用的非常少,此处简单了解下语法格式。
java
public class OutClass {
int a = 10;
public void method() {
int b = 10;
//局部内部类:定义在方法体内部
//不能被public、static等访问限定符修饰
class InnerClass {
public void methodInnerClass() {
System.out.println(a);
System.out.println(b);
}
}
//只能在该方法体内部使用,其他位置都不能用
InnerClass innerClass = new InnerClass();
innerClass.methodInnerClass();
}
public static void main(String[] args) {
// OutClass.InnerClass innerClass = null;编译失败
}
}
【注意事项】
- 局部内部类只能在所定义的方法体内部使用
- 不能被public、static等修饰符修饰
- 编译器也有自己独立的字节码文件,命名格式:外部类名字 $ 数字内部类名字.class
- 几乎不会使用
3.5 匿名内部类
实现接口并对其进行重写

匿名内部类是Java中一种特殊的嵌套类,它没有类名,在声明的同时完成实例化。匿名内部类通常用于创建只需使用一次的类.
语法形式:
java
new SuperType(constructor-arguments) {
//类体
};
SuperType可以是接口、抽象类或具体类。
示例代码:
java
interface Greeting {
void greet();
}
public class Test {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
@Override
public void greet() {
System.out.println("Hello!");
}
};
greeting.greet();
}
}
- 匿名内部类当中可以定义和正常类一样的成员变量,但是和正常类一样都不能直接包含执行语句。
java
interface Greeting {
void greet();
}
public class Derived extends Base {
public static void main(String[] args) {
Greeting greeting = new Greeting() {
public static int a = 10;
a = 100; //error
@Override
public void greet() {
System.out.println("Hello!" + a);
}
};
greeting.greet();
}
}
4. Object类
是所有类的父类,默认继承
Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
范例:使用Object接收所有类的对象
java
class Person {}
class Student {}
public class Test {
public static void main(String[] args) {
function(new Person());
function(new Student());
}
public static void function(Object obj) {
System.out.println(obj);
}
}
//执行结果:
Person@1b6d3586
Student@4554617c
所以在开发之中,Object类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。如下:

对于整个Object类中的方法需要实现全部掌握。 本小节当中,主要来熟悉这几个方法:toString()方法,equals()方法,hashcode()方法
4.1 获取对象信息
如果要打印对象中的内容,可以直接重写Object类中的toString()方法,之前已经讲过了,此处不再累赘。
java
// Object类中的toString()方法实现:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
4.2 对象比较equals方法
在Java中,进行比较时:
a. 如果左右两侧是基本类型变量,比较的是变量中值是否相同
b. 如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同
c. 如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的:
右键Generate选择<font style="color:#DF2A3F;">equals</font>快速生成<font style="color:#DF2A3F;">equals</font>
java
// Object类中的equals方法
public boolean equals(Object obj) {
return (this == obj); //使用引用中的地址直接来进行比较
}
java
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.age = age;
this.name = name;
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person("gaobo", 20);
Person p2 = new Person("gaobo", 20);
int a = 10;
int b = 10;
System.out.println(a == b); //输出true
System.out.println(p1 == p2); //输出false
System.out.println(p1.equals(p2)); //输出false
}
}
Person类重写equals方法后,然后比较:
java
class Person {
...
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
//不是Person类对象
if (!(obj instanceof Person)) {
return false;
}
Person person = (Person) obj; //向下转型,比较属性值
return this.name.equals(person.name) && this.age == person.age;
}
}
结论:比较对象中内容是否相同的时候,一定要重写equals方法
4.3 hashcode方法
回忆刚刚的toString方法的源码:
java
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
我们看到了hashCode()这个方法,他帮我算了一个具体的对象位置,这里面涉及数据结构,但是我们还没学数据结构,没法讲述,所以我们只能说它是个内存地址。然后调用Integer.toHexString()方法,将这个地址以16进制输出。
hashcode方法源码:
java
public native int hashCode();
该方法是一个native方法,底层是由C/C++代码写的。我们看不到。
我们认为两个名字相同,年龄相同的对象,将存储在同一个位置,如果不重写hashcode()方法,我们可以来查看示例代码:
java
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public class TestDemo4 {
public static void main(String[] args) {
Person per1 = new Person("gaobo", 20);
Person per2 = new Person("gaobo", 20);
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
//执行结果
460141958
1163157884
注意事项:两个对象的hash值不一样。
像重写equals方法一样,我们也可以重写hashcode()方法。此时我们再来看看。
java
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class TestDemo4 {
public static void main(String[] args) {
Person per1 = new Person("gaobo", 20);
Person per2 = new Person("gaobo", 20);
System.out.println(per1.hashCode());
System.out.println(per2.hashCode());
}
}
//执行结果
460141958
460141958
注意事项:哈希值一样。
结论:
- hashcode方法用来确定对象在内存中存储的位置是否相同
- 事实上hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置。
5. 小试牛刀
- 关于Java抽象类,以下哪个陈述是错误的?
A. 抽象类可以包含非抽象方法
B. 抽象类可以被实例化
C. 抽象类可以有构造方法
D. 一个类可以继承多个抽象类 - 关于Java接口,以下哪个说法是正确的?
A. 接口中的所有方法都必须是抽象的
B. 接口可以包含静态方法
C. 接口可以有私有方法
D. 一个类只能实现一个接口 - 以下关于内部类的描述,哪个是错误的?
A. 静态内部类可以访问外部类的静态成员
B. 非静态内部类可以直接访问外部类的所有成员
C. 局部内部类可以访问所在方法的final局部变量
D. 匿名内部类可以继承其他类 - 关于抽象类和接口的区别,以下哪个说法是正确的?
A. 抽象类可以有构造方法,而接口不能
B. 一个类可以实现多个抽象类,但只能实现一个接口
C. 接口中的所有方法都必须是public的,而抽象类没有这个限制
D. 抽象类中的方法必须都是抽象的
答案解析:
- B D
- B (C)在jdk8里面不可以有私有方法,现在jdk如17可以
- D 匿名内部类只能继承一个类或实现一个接口,不能同时继承类和实现接口
局部内部类实现代码:

- A C 接口中的方法也可以是私有的
四、字符串-String类
1. String类的重要性
在C语言中已经涉及到字符串了,但是在C语言中要表示字符串只能使用字符数组或者字符指针,可以使用标准库提供的字符串系列函数完成大部分操作,但是这种将数据和操作数据方法分离开的方式不符合面向对象的思想,而字符串应用又非常广泛,因此Java语言专门提供了String类。
2. String原理介绍
2.1 字符串构造
String类提供的构造方式非常多,常用的就以下三种:
java
public static void main(String[] args) {
// 使用字符串常量进行赋值
String s1 = "hello bit";
System.out.println(s1);
// 直接new String对象
String s2 = new String("hello bit");
System.out.println(s1);
// 使用字符数组进行构造
char[] array = { 'h', 'e', 'l', 'l', 'o', 'b', 'i', 't'};
String s3 = new String(array);
System.out.println(s1);
// 使用字节数组构造对象
byte[] bytes = {97, 98, 99, 100};
String s4 = new String(bytes);
System.out.println(str); // 这里应该是s4
}
其他构造方法需要用到时,大家参考Java在线文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html
查看String源码,我们观察到:String是引用类型,内部并不存储字符串本身。

2.2 字符串常量池(StringTable)
想要了解字符串的布局,我们首先得了解一个新的内存叫做:字符串常量池(StringTable). 字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(一种高效用来进行查找的数据结构,后续给大家详细介绍),不同JDK版本下字符串常量池的位置以及默认大小是不同的:
| JDK版本 | 存储位置 | 默认大小 |
|---|---|---|
| JDK6 | 方法区 | 1009 |
| JDK7 | 堆 | 60013 |
| JDK8 | 堆 | 60013 |
关于方法区、堆等内存结构的具体局部,后续JVM中会给大家详细介绍。
关于"池"的理解: "池"是编程中的一种常见的,重要的++提升效率++的方式,我们会在未来的学习中遇到各种"内存池","线程池","数据库连接池"...比如:家里给大家打生活费的方式
- 家里经济拮据,每月定时打生活费,有时可能会晚,最差情况下可能需要向家里张口要,速度慢
- 家里有矿,一次性打一年的生活费放到银行卡中,自己随用随取,速度非常快
方式2,就是池化技术的一种示例,钱放在卡上,随用随取,效率非常高。常见的池化技术比如:数据库连接池、线程池等。
2.3 字符串内存存储
示例1:
java
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);
}

- str1在进行存储的时候,"abc"会先存储到字符串常量池当中
- 当str2再次存储的时候,先会检查字符串常量池当中是否存在 "abc"常量,如果存在,则不再重复存储。所以会出现
<font style="color:#DF2A3F;">str1==str2</font>,是两个对象指向的同一块地址
示例2:
java
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2);
}

两个string都是先指向value,然后再从字符串常量池中查看并且拿取内容,但分别指向的value地址不一样,所以输出false

- 第一次存储的时候,会将"abc"存储到常量池当中
- 每次new都会在堆中实例化新的对象
- 存储str2的时候,会使用常量池的"abc"对象进行存储
3. 常用方法介绍
3.1 String对象的比较
字符串的比较是常见操作之一,比如:当你登录一个网站的时候,输入了用户名和密码之后,需要后台进行比较和对比。
3.1.1 ==比较是否引用同一个对象
注意:对于内置类型,比较的是变量中的值;**对于引用类型比较的是引用中的地址。**
java
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
}
3.1.2 **<font style="color:#DF2A3F;">equals</font>**方法:按照字典序比较
比较是否相同
字典序:字符大小的顺序
String类重写了父类Object中equals方法,Object中equals默认按照==比较,String重写equals方法后,按照如下规则进行比较,比如:<font style="color:rgb(6, 10, 38);">s1.equals(s2)</font>
JDK17当中的源码部分:
java
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString) &&
(!COMPACT_STRINGS || this.coder == aString.coder) &&
StringLatin1.equals(value, aString.value);
}
@IntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
使用示例:
java
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
}
3.1.3 **<font style="color:rgb(6, 10, 38);">compareTo</font>**方法: 按照字典序进行比较
与equals不同的是,equals返回的是boolean类型,而compareTo返回的是int类型。具体比较方式:
- 先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值
- 如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值
java
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");
System.out.println(s1.compareTo(s2)); // 不同输出字符差值-1
System.out.println(s1.compareTo(s3)); // 相同输出 0
System.out.println(s1.compareTo(s4)); // 前k个字符完全相同,输出长度差值-3
}
3.1.4 **<font style="color:#DF2A3F;">compareToIgnoreCase</font>**方法**:与compareTo方式相同,但是****忽略大小写比较**
java
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");
System.out.println(s1.compareToIgnoreCase(s2)); // 不同输出字符差值-1
System.out.println(s1.compareToIgnoreCase(s3)); // 相同输出 0
System.out.println(s1.compareToIgnoreCase(s4)); // 前k个字符完全相同,输出长度差值-3
}
3.2 字符串查找
字符串查找也是字符串中非常常见的操作,String类提供的常用查找的方法:

java
public static void main(String[] args) {
//定义s这个字符串
String s = "aaabbbcccaaabbbccc";
s.charAt(3):返回index位置上字符
System.out.println(s.charAt(3)); //下标为3的字符 'b'
s.indexOf('c'):返回字符c第一次出现的位置
System.out.println(s.indexOf('c')); // 6
s.indexOf('c', 10):返回从下标10开始找字符c第一次出现的位置
System.out.println(s.indexOf('c', 10)); // 15
s.indexOf("bbb"):返回字符串bbb第一次出现的位置
System.out.println(s.indexOf("bbb")); // 3
s.indexOf("bbb", 10):返回从下标10开始找字符串bbb第一次出现的位置
System.out.println(s.indexOf("bbb", 10)); // 12
s.lastIndexOf('c'):从后往前找,返回字符c第一次出现的位置
System.out.println(s.lastIndexOf('c')); // 17
s.lastIndexOf('c', 10):从下标10开始找,从后往前找字符c第一次出现的位置
System.out.println(s.lastIndexOf('c', 10)); // 8
s.lastIndexOf("bbb"):从后往前找,返回字符串bbb第一次出现的位置
System.out.println(s.lastIndexOf("bbb")); // 12
s.lastIndexOf("bbb", 10):从下标10开始找,从后往前找字符串bbb第一次出现的位置
System.out.println(s.lastIndexOf("bbb", 10)); // 3
}
3.3 转换
3.3.1 数值和字符串转化
java
public static void main(String[] args) {
// 数字转字符串
String s1 = String.valueOf(1234);
String s2 = String.valueOf(12.34);
String s3 = String.valueOf(true);
//定义类中的对象后,给出构造方法的同时还要重写toString方法,否则输出的是对象的地址
String s4 = String.valueOf(new Student("Hanmeimei", 18));
System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
System.out.println(s4);
System.out.println("=================================");
// 字符串转数字
// 注意:Integer、Double等是Java中的包装类型,这个后面会讲到
int data1 = Integer.parseInt("1234");
double data2 = Double.parseDouble("12.34");
System.out.println(data1);
System.out.println(data2);
}
3.3.2 大小写转换
大小写转换产生的是新的对象,都不会改变字符串常量
java
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO";
// 小写转大写
System.out.println(s1.toUpperCase());
// 大写转小写
System.out.println(s2.toLowerCase());
}
3.3.3 字符串转数组
java
public static void main(String[] args) {
String s = "hello";
// 字符串转数组
char[] ch = s.toCharArray();
for (int i = 0; i < ch.length; i++) {
System.out.print(ch[i]);
}
System.out.println();
// 数组转字符串
String s2 = new String(ch);
System.out.println(s2);
}
数组转字符串例2

3.3.4 格式化
java
public static void main(String[] args) {
String s = String.format("%d-%d-%d", 2019, 9, 14);
System.out.println(s);
}
3.4 字符串替换
使用一个指定的新的字符串(没有改变原来的)替换掉已有的字符串数据,可用的方法如下:

java
String str = "helloworld";
System.out.println(str.replaceAll("l", "_"));
System.out.println(str.replaceFirst("l", "_"));
注意事项: 由于字符串是不可变对象,替换不修改当前字符串,而是产生一个新的字符串.
3.5 字符串拆分
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串。可用方法如下:

代码示例: 实现字符串的拆分处理
java
String str = "hello world hello bit";
String[] result = str.split(" "); //按照空格拆分后得放到一个数组里,所以用数组接收
for (String s : result) {
System.out.println(s);
}
代码示例: 字符串的部分拆分
java
String str = "hello world hello bit";
String[] result = str.split(" ", 2); //按照空格拆分,分成两组
for (String s : result) {
System.out.println(s);
}
拆分是特别常用的操作.一定要重点掌握.另外有些特殊字符作为分割符可能无法正确切分,需要加上转义.
代码示例: 拆分IP地址
java
String str = "192.168.1.1";
String[] result = str.split("\\.");
for (String s : result) {
System.out.println(s);
}

代码示例: 多次拆分
java
String str = "name=zhangsan&age=18";
String[] result = str.split("&"); //按照"&"来分隔
for (int i = 0; i < result.length; i++) {
String[] temp = result[i].split("="); //按照"="来分隔
System.out.println(temp[0] + "=" + temp[1]);
}
3.6 字符串截取
从一个完整的字符串之中截取出部分内容。可用方法如下:

java
String str = "helloworld";
System.out.println(str.substring(5)); //所以输出的是hello,从0下标开始,第5位截取
System.out.println(str.substring(0, 5));
注意事项:
- 索引从0开始
- 注意前闭后开区间的写法, substring(0,5)表示包含0号下标的字符,不包含5号下标,换言之就是左闭右开
**<font style="color:#DF2A3F;background-color:#FBF5CB;">[0,5) </font>**的写法
3.7 去除左右两边的空格

java
String str = " hello world ";
System.out.println("[" + str + "]");
System.out.println("[" + str.trim() + "]");
3.8 intern方法
- 当调用
<font style="color:rgb(6, 10, 38);">intern()</font>方法时,如果字符串常量池中已经包含一个等于此String对象的字符串(由equals(Object)方法确定),则返回常量池中的字符串。 - 否则,将此String对象添加到常量池中,并返回此String对象的引用。
java
public static void main(String[] args) {
char[] ch = new char[]{'a', 'b', 'c'};
String s1 = new String(ch); // s1对象并不在常量池中
// s1.intern(); // s1.intern();调用之后,会将s1对象的引用放入到常量池中
String s2 = "abc"; // "abc"在常量池中存在了,s2创建时直接用常量池中"abc"的引用
System.out.println(s1 == s2);
}
// 输出 false
// 将上述方法打开之后,就会输出true

加上**<font style="color:rgb(6, 10, 38);">s1.intern()</font>**后图解:【手动入池】

4. 字符串的不可变性
String是一种不可变对象.字符串中的内容是不可改变。字符串不可被修改,是因为:
- String类在设计时就是不可改变的,String类实现描述中已经说明了

- 所有涉及到可能修改字符串内容的操作都是创建一个新对象,改变的是新对象

- String之所以不可以被修改的原因是内部数组因为为私有成员

此时final修饰该数组表示,该数组不能指向其他的对象,但是可以修改对象内部的数据。其次String类没有提供任何方法来修改<font style="color:rgb(6, 10, 38);">value</font>数组的内容。
<font style="color:rgb(6, 10, 38);">@Stable</font>:这是一个JVM优化注解,提示JVM这个字段的值在初始化后不会改变,可以进行某些优化。
5. 字符串修改
对于String类本身来说,是不可以修改的。形如如下代码:
java
public static void main(String[] args) {
String s = "hello";
s = s + " world";
System.out.println(s); // 输出:hello world
}
在这个例子中,**<font style="color:#DF2A3F;">s + " world"</font>**创建了一个新的String对象 ,而原来的"Hello"对象保持不变. 对于String的拼接来说,如果是在循环当中会产生很多的临时对象。此时Java提供了StringBuffer和StringBuilder。
我们通过下面的例子直观感受一下运行的效率:
java
public static void main(String[] args) {
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < 10000; ++i) {
s += i;
}
long end = System.currentTimeMillis();
System.out.println(end - start);
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer("");
for (int i = 0; i < 10000; ++i) {
sbf.append(i);
}
end = System.currentTimeMillis();
System.out.println(end - start);
start = System.currentTimeMillis();
StringBuilder sbd = new StringBuilder();
for (int i = 0; i < 10000; ++i) {
sbd.append(i);
}
end = System.currentTimeMillis();
System.out.println(end - start);
}
可以看到在对String类进行拼接时,效率是非常慢,因此:尽量避免对String的直接需要,如果要修改建议尽量使用StringBuffer或者StringBuilder。
6. StringBuilder和StringBuffer
6.1 StringBuilder的介绍
由于String的不可更改特性,为了方便字符串的修改,Java中又提供StringBuilder和StringBuffer类。这两个类大部分功能是相同的,这里介绍StringBuilder常用的一些方法,其它需要用到了大家可参阅 https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StringBuilder.html

java
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("hello");
StringBuilder sb2 = sb1;
// 追加:即尾插-->字符、字符串、整形数字
sb1.append(' '); // hello
sb1.append("world"); // hello world
sb1.append(123); // hello world123
System.out.println(sb1); // hello world123
System.out.println(sb1 == sb2); // true
System.out.println(sb1.charAt(0)); // 获取0号位上的字符 h
System.out.println(sb1.length()); // 获取字符串的有效长度14
System.out.println(sb1.capacity()); // 获取底层数组的总大小
sb1.setCharAt(0, 'H'); // 设置任意位置的字符 Hello world123
sb1.insert(0, "Hello world!!!"); // Hello world!!!Hello world123
System.out.println(sb1);
System.out.println(sb1.indexOf("Hello")); // 获取Hello第一次出现的位置
System.out.println(sb1.lastIndexOf("hello")); // 获取hello最后一次出现的位置
sb1.deleteCharAt(0); // 删除首字符
sb1.delete(0, 5); // 删除[0, 5)范围内的字符
String str = sb1.substring(0, 5); // 截取[0, 5)区间中的字符 以String的方式返回
System.out.println(str);
sb1.reverse(); // 字符串逆转
str = sb1.toString(); // 将StringBuffer以String的方式返回
System.out.println(str);
}

从上述例子可以看出:String和StringBuilder最大的区别在于**String的内容无法修改,而StringBuilder的内容可以修改****。**频繁修改字符串的情况考虑使用StringBuilder。
注意:String和StringBuilder类不能直接转换。如果要想互相转换,可以采用如下原则:
- String变为StringBuilder: 利用StringBuilder的构造方法或append()方法


- StringBuilder变为String: 调用
**<font style="color:#DF2A3F;">toString()</font>**方法。
6.2 StringBuffer的介绍
StringBuffer和StringBuilder从方法的功能上来说没有太大区别,这里不做单独介绍,可以查看官方文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StringBuffer.html
唯一的区别在于方法的声明上会存在一下区别:


StringBuffer采用同步处理,属于线程安全操作(相当于一把锁,但是如果都是用StringBuffer,就会频繁的上锁开锁,消耗资源);而StringBuilder未采用同步处理,属于线程不安全操作
7.综上方法总结
https://blog.csdn.net/2401_87083249/article/details/159016461?spm=1001.2014.3001.5501
8. 小试牛刀-String类oj
1. 第一个只出现一次的字符
387. 字符串中的第一个唯一字符 - 力扣(LeetCode)
java
class Solution {
public int firstUniqChar(String s) {
int[] count = new int[256];
// 统计每个字符出现的次数
for (int i = 0; i < s.length(); ++i) {
count[s.charAt(i)]++;
}
// 找第一个只出现一次的字符
for (int i = 0; i < s.length(); ++i) {
if (1 == count[s.charAt(i)]) {
return i;
}
}
return -1;
}
}
2. 字符串最后一个单词的长度
java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
// 循环输入
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
// 获取一行单词
String s = sc.nextLine();
// 1. 找到最后一个空格
// 2. 获取最后一个单词:从最后一个空格+1位置开始,一直截取到末尾
// 3. 打印最后一个单词长度
int len = s.substring(s.lastIndexOf(' ') + 1, s.length()).length();
System.out.println(len);
}
sc.close();
}
}
3. 验证回文串
java
class Solution {
public static boolean isValidChar(char ch) {
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) {
return true;
}
return false;
}
public boolean isPalindrome(String s) {
// 将大小写统一起来
s = s.toLowerCase();
int left = 0, right = s.length() - 1;
while (left < right) {
// 1. 从左侧找到一个有效的字符
while (left < right && !isValidChar(s.charAt(left))) {
left++;
}
// 2. 从右侧找一个有效的字符
while (left < right && !isValidChar(s.charAt(right))) {
right--;
}
if (s.charAt(left) != s.charAt(right)) {
return false;
} else {
left++;
right--;
}
}
return true;
}
}
五、异常
1. 异常的概念与体系结构
1.1 异常的概念
异常是在程序执行过程中发生的一种特殊情况或错误状态。它打断了程序的正常流程,需要特别处理。在日常开发中,程序员绞尽脑汁将代码写的尽善尽美,在程序运行过程中,难免会出现一些奇奇怪怪的问题。有时通过代码很难去控制,比如:数据格式不对、网络不通畅、内存报警等。抛出异常实际上就是抛出一个类(点进去异常源码发现)
在Java中,将程序执行过程中发生的不正常行为称为异常,比如之前写代码时经常遇到的:
- 算术异常
java
public static void main(String[] args){
System.out.println(10 / 0);
}
java
Exception in thread "main" java.lang.ArithmeticException: / by zero
at LibrarySystem.main(LibrarySystem.java:97)
- 数组越界异常
java
public static void main(String[] args){
int[] array = {1, 2, 3};
System.out.println(array[100]);
}
java
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 100 out of bounds for length 3
at LibrarySystem.main(LibrarySystem.java:98)
- 空指针异常
java
public static void main(String[] args){
int[] array = null;
System.out.println(array.length);
}
java
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "array" is null
at LibrarySystem.main(LibrarySystem.java:98)
从上述过程中可以看到,java中不同类型的异常,都有与其对应的类来进行描述。
1.2 异常的体系结构
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:

从上图中可以看到:
**<font style="color:#DF2A3F;">Throwable</font>**:是异常体系的顶层类,其派生出两个重要的子类,**<font style="color:#DF2A3F;">Error</font>**和**<font style="color:#DF2A3F;">Exception</font>****<font style="color:rgb(6, 10, 38);">Error</font>**:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:**<font style="color:rgb(6, 10, 38);">StackOverflowError</font>**和**<font style="color:rgb(6, 10, 38);">OutOfMemoryError</font>**,一旦发生回力乏术。**<font style="color:rgb(6, 10, 38);">Exception</font>**:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception。
1.3 异常的分类
异常可能在编译时发生,也可能在程序运行时发生,根据发生的时机不同,可以将异常分为:
- ++编译时异常++
在程序编译期间发生的异常,称为编译时异常,也称为受检查异常(Checked Exception)
java
public class Person {
private String name;
private String gender;
int age;
// 想要让该类支持深拷贝,覆写Object类的clone方法即可
@Override
public Person clone() {
return (Person) super.clone();
}
}
编译时报错:
Error:(17, 35) java: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出
- ++运行时异常++
在程序执行期间发生的异常,称为运行时异常,也称为非受检查异常(Unchecked Exception)
RunTimeException****以及其子类对应的异常,都称为运行时异常。 比如:<font style="color:rgb(6, 10, 38);">NullPointerException</font>、<font style="color:rgb(6, 10, 38);">ArrayIndexOutOfBoundsException</font>、<font style="color:rgb(6, 10, 38);">ArithmeticException</font>。
注意 :编译时出现的语法性错误,不能称之为异常。例如将<font style="color:rgb(6, 10, 38);">System.out.println</font>拼写错了,写成了<font style="color:rgb(6, 10, 38);">system.out.println</font>.此时编译过程中就会出错,这是"编译期"出错。而运行时指的是程序已经编译通过得到class文件了,再由JVM执行过程中出现的错误.
2. 异常的处理
2.1 防御式编程
错误在代码中是客观存在的.因此我们要让程序出现问题的时候及时通知程序猿.主要的方式
- LBYL : Look Before You Leap.在操作之前就做充分的检查.即:事前防御型
java
boolean ret = false;
ret = 登陆游戏();
if (!ret) {
处理登陆游戏错误;
return;
}
ret = 开始匹配();
if (!ret) {
处理匹配错误;
return;
}
ret = 游戏确认();
if (!ret) {
处理游戏确认错误;
return;
}
ret = 选择英雄();
if (!ret) {
处理选择英雄错误;
return;
}
ret = 载入游戏画面();
if (!ret) {
处理载入游戏错误;
return;
}
......
缺陷:正常流程和错误处理流程代码混在一起,代码整体显的比较混乱。
- EAFP : It's Easier to Ask Forgiveness than Permission. "事后获取原谅比事前获取许可更容易".也就是先操作,遇到问题再处理.即:事后认错型

java
try {
登陆游戏();
开始匹配();
游戏确认();
选择英雄();
载入游戏画面();
...
} catch (登陆游戏异常) {
处理登陆游戏异常;
} catch (开始匹配异常) {
处理开始匹配异常;
} catch (游戏确认异常) {
处理游戏确认异常;
} catch (选择英雄异常) {
处理选择英雄异常;
} catch (载入游戏画面异常) {
处理载入游戏画面异常;
}
......
优势:正常流程和错误流程是分离开的,程序员更关注正常流程,代码更清晰,容易理解代码
异常处理的核心思想就是EAFP。
在Java中,异常处理主要的5个关键字: **<font style="color:rgb(6, 10, 38);">throw</font>**、 **<font style="color:rgb(6, 10, 38);">try</font>**、 **<font style="color:rgb(6, 10, 38);">catch</font>**、 **<font style="color:rgb(6, 10, 38);">finally</font>**、 **<font style="color:rgb(6, 10, 38);">throws</font>**。
2.2 异常的抛出-throw
还可以抛出自定义异常
在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调用者,比如:参数检测。在Java中,可以借助<font style="color:rgb(6, 10, 38);">throw</font>关键字,抛出一个指定的异常对象,将错误信息告知给调用者。具体语法如下:
java
throw new XXXException("异常产生的原因");
【需求】:实现一个获取数组中任意位置元素的方法。
java
public static int getElement(int[] array, int index) {
if (null == array) {
throw new NullPointerException("传递的数组为null");
}
if (index < 0 || index >= array.length) {
throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
}
return array[index];
}
public static void main(String[] args) {
int[] array = {1, 2, 3};
getElement(array, 3);
}
【注意事项】
<font style="color:#DF2A3F;">throw</font>必须写在方法体内部- 抛出的对象必须是Exception或者Exception的子类对象
- 如果抛出的是
<font style="color:rgb(6, 10, 38);">RunTimeException</font>或者<font style="color:rgb(6, 10, 38);">RunTimeException</font>的子类,则可以不用处理,直接交给JVM来处理 - 如果抛出的是编译时异常,用户必须处理,否则无法通过编译

如果声明异常了,下面就不会划红线了
- 异常一旦抛出,其后的代码就不会执行
2.3 异常的声明-throws
<font style="color:#DF2A3F;">throws</font> 关键字用于在方法声明中列出该方法可能抛出的异常,它告诉调用者这个方法可能会抛出某些异常,调用者需要处理这些异常。使用 <font style="color:rgb(6, 10, 38);">throws</font> 实际上是将异常的处理责任转移给了调用该方法的代码。
语法格式:
java
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2... {}
- 多个异常用逗号隔开
需求:加载指定的配置文件config.ini
java
public class Config {
File file;
/*
* FileNotFoundException: 编译时异常,表明文件不存在
* 此处不处理,也没有能力处理,应该将错误信息报告给调用者,让调用者检查文件名字是否给错误了
*/
public void OpenConfig(String filename) throws FileNotFoundException {
if (filename.equals("config.ini")) {
throw new FileNotFoundException("配置文件名字不对");
}
// 打开文件
}
public void readConfig() {
}
}
【注意事项】
<font style="color:#DF2A3F;">throws</font>必须跟在方法的参数列表之后- 声明的异常必须是Exception或者Exception的子类
- 方法内部如果抛出了多个异常,
**<font style="color:#DF2A3F;">throws</font>**之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。
java
public class Config {
File file;
// FileNotFoundException继承自 IOException
public void OpenConfig(String filename) throws IOException {
if (filename.endsWith(".ini")) {
throw new IOException("文件不是.ini文件");
}
if (filename.equals("config.ini")) {
throw new FileNotFoundException("配置文件名字不对");
}
// 打开文件
}
public void readConfig() {
}
}
- 调用声明抛出异常的方法时,如果该异常是编译时异常/受查异常时,调用者必须对该异常进行处理,或者继续使用
<font style="color:rgb(6, 10, 38);">throws</font>抛出
java
public static void main(String[] args) throws IOException {
Config config = new Config();
config.openConfig("config.ini");
}
将光标放在抛出异常方法上,<font style="color:rgb(6, 10, 38);">alt+Insert</font>快速处理:

2.4 异常的捕获-try-catch
只要异常没有处理,交给了JVM,此时JVM就会立即停止程序
捕获并处理异常。<font style="color:rgb(6, 10, 38);">throws</font>对异常并没有真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。如果真正要对异常进行处理,就需要try-catch。
语法格式:
java
try {
// 将可能出现异常的代码放在这里
} catch (要捕获的异常类型 e) {
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,
// 或者是try中抛出异常的基类时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
} [catch (异常类型 e) { // e的意思是把运行时出现异常的代码行数输出出来,更直观
// 对异常进行处理
}] [finally {
// 此处代码一定会被执行到
}]
// 后序代码
// 当异常被捕获到时,异常就被处理了,这里的后序代码一定会执行
// 如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码就不会被执行
注意:
<font style="color:rgb(6, 10, 38);">[]</font>中表示可选项,可以添加,也可以不用添加<font style="color:rgb(6, 10, 38);">try</font>中的代码可能会抛出异常,也可能不会
**需求:**读取配置文件,如果配置文件名字不是指定名字,抛出异常,调用者进行异常处理
java
public class Config {
File file;
public void openConfig(String filename) throws FileNotFoundException {
if (!filename.equals("config.ini")) {
throw new FileNotFoundException("配置文件名字不对");
}
// 打开文件
}
public void readConfig() {
}
public static void main(String[] args) {
Config config = new Config();
try {
config.openConfig("config.txt");
System.out.println("文件打开成功");
} catch (IOException e) {
// 异常的处理方式
// System.out.println(e.getMessage()); // 只打印异常信息
// System.out.println(e); // 打印异常类型:异常信息
e.printStackTrace(); // 打印信息最全面
}
// 一旦异常被捕获处理了,此处的代码会执行
System.out.println("异常如果被处理了,这里的代码也可以执行");
}
}
