当前位置: 首页 > news >正文

Java从入门到精通!第五天(面向对象(二))

三、继承性和多态性

1. 面向对象编程的第二大基本特性:继承性,子类自动共享父类属性和方法的机制

示例:引出继承性

package com.edu.beans;

import java.util.Date;

public class Student {
public String name;
public int age;
public Date birthday;
public String school;

public String getInfo() {
return name + "," + age + "," + birthday + "," + school;
}
}

我们在定义 Person 类和 Student 类的时候,Student 类做了和 Person 类相同的事情,就是定义了相同的:

   public String name;
public int age;
public Date birthday;

这几个属性。

那么有没有一种机制,让 Student 类可以直接使用 Person 类的属性和方法,回答是肯定的,使用继承机制就可以。

2. 继承的语法:

Person.java:父类

package com.edu.beans;
​
import java.util.Date;
​
public class Person {public String name;public int age;public Date birthday;public String getInfo() {return name + "," + age + "," + birthday;}
}

Teacher.java:子类

package com.edu.beans;
​
//Teacher通过 extends 继承了 Person 类之后,就可以共享 Person 的属性和方法
public class Teacher extends Person{public double sal;//扩展的属性public void say() { //扩展的方法//这里的name,age,birthday就是从父类 Person 中继承过来的System.out.println("我是一名教师,name=" + name + ",age=" + age + ",birthday=" + birthday + ",sal=" + sal);}
}

测试类:

package com.edu.beans;
​
import java.util.Date;
​
public class TeacherDemo {
​public static void main(String[] args) {// TODO Auto-generated method stubTeacher t = new Teacher();t.name = "张三";//从父类Person继承过来的name属性t.age = 23;//从父类Person继承过来的age属性t.birthday = new Date();//从父类Person继承过来的birthday属性t.sal = 20000;//子类Teacher自己扩展出来的sal属性t.say();//调用子类Teacher扩展出来的say()方法String info = t.getInfo();//调用父类Person的方法System.out.println(info);}
​
}

注意:子类不能继承父类私有(private)的成员,原因是private的成员仅限于在类的内部访问。

练习:定义Animal类,包含一个shout()方法,子类Dog和Cat继承了Animal类,并扩展出了eat()方法,分别创建子类对象调用子类和父类的方法。

3. 继承的作用

(1) 继承减少了代码的冗余,提供了代码的复用性

(2) 更有利于功能的扩展

(3) 为多态提供了前提(多态是建立在继承的基础之上的)

4. Java 中只支持单继承(上例就是单继承)和多层继承,不支持多重继承

示例:多层继承

Demo1.java:

package com.edu.ext;
​
public class Demo1 {public void say1() {System.out.println("我是Demo1");}
}

Demo2.java:

package com.edu.ext;
​
public class Demo2 extends Demo1{public void say2() {System.out.println("我是Demo2");}
}

Demo3.java:

package com.edu.ext;
​
public class Demo3 extends Demo2{public void say3() {System.out.println("我是Demo3");}
}

测试类:DemoTest.java

package com.edu.ext;
​
public class DemoTest {
​public static void main(String[] args) {// TODO Auto-generated method stubDemo3 demo3 = new Demo3();demo3.say3();//调用自己扩展的方法demo3.say2();//调用直接父类Demo2的方法demo3.say1();//调用间接父类Demo1的方法}
​
}

如果将代码改为多重继承:在 Java 中是不允许的(但是C++是允许的):

说到继承不得不说组合,组合也是一种代码复用的方式,组合就是将一个或多个类的引用置于新类中即可,例如:

package com.edu.ext;

public class ComposeDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Car car = new Car();
car.left.open();
car.left.close();
car.engine.start();
car.engine.stop();
}

}

class Engine {//引擎
public void start() {}
public void stop() {}
}

class Wheel { //轮子
public void roll() {}
}

class Window { //车窗
public void rollup() {}
public void rolldown() {}
}

class Door { //车门
public Window window = new Window();//组合:也可以复用代码
public void open() {}
public void close() {}
}

class Car { //汽车
//组合
public Engine engine = new Engine();
public Wheel[] wheels = new Wheel[4];
public Door left = new Door();
public Door right = new Door();

public Car() {
// TODO Auto-generated constructor stub
for (int i = 0; i < wheels.length; i++) {
wheels[i] = new Wheel();
}
}
}

从上可知,组合也能够复用代码,那么什么时候用继承,什么时候用组合:“is-a”(是一个)关系用继承,”has-a”(有一个)关系用组合。

5. 方法重写(Override/Overwrite)

(1) 子类可以根据需求改写父类的方法,称为方法重写(方法覆盖/方法重置),通过子类对象调用该方法的时候,就会调用重写(覆盖)之后的方法。

(2) 方法重写的要求

1) 子类重写的父类的方法具有相同的方法名和参数列表

2) 子类重写的父类的方法的返回值要一致

3) 子类重写的方法的访问权限不能低于父类方法

访问权限递减的顺序是:public > protected > 缺省 > private

4) 子类方法抛出的异常不能大于父类被重写方法的异常

示例:

示例:

package com.edu.ext;
​
public class OverRideDemo {
​public static void main(String[] args) {// TODO Auto-generated method stubParent p = new Parent();p.show("大明");//通过父类对象调用父类的show方法Son s = new Son();//由于子类重写了父类的show方法,那么子类的show方法会覆盖父类的show方法,通过子类对象调用时,会调用子类覆盖后的show方法//而不会调用父类的show方法s.show("小明");}
​
}
​
class Parent {public void show(String name) {System.out.println("我是父亲,name=" + name);}
}
​
class Son extends Parent {public void show(String name) {//该方法重写了/覆盖了父类的show方法System.out.println("我是儿子,name=" + name);}
}

不符合方法重写要求的示例1:

不符合方法重写要求的示例2:

不符合方法重写要求示例3:

6. 访问修饰符

(1) 访问修饰符定义了类的成员(属性和方法)的访问权限

主要有public、protected、缺省、private 四种权限,用来限定对象对该类成员的访问权限,我们将其访问权限列出如下表:

修饰符 类内部 同一个包 不同包的子类 同一个工程

类的内部:

同一个包,不同的类:

不同包,不同类:

不同包,不同类,有继承关系:

7. super 关键字

(1) super 关键字在 Java 中代表父类的引用,指向父类对象

1) super 可以用于访问父类的属性

2) super 可以用于访问父类的方法

3) super 还可以用在构造函数中调用父类的构造函数

示例:

package com.edu.sup;

public class SuperDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Son s1 = new Son();
s1.show();//会发生方法覆盖

Son s2 = new Son(21);
s2.show();//会发生方法覆盖
}

}

class Parent {//父类/超类
private String name;

public Parent() {
// TODO Auto-generated constructor stub
}

    public Parent(String name) {
this.name = name;
}

    public String getName() {
return name;
}

    public void setName(String name) {
this.name = name;
}

public void show() {
System.out.println("父类,name=" + name);
}
}

class Son extends Parent {//子类/派生类
private int age;

public Son() {
// TODO Auto-generated constructor stub
}

    public Son(int age) {
super("张三");//通过super关键字调用父类的带参构造函数,注意这里也不能直接通过父类构造函数名来调用
this.age = age;
}

    public int getAge() {
return age;
}

    public void setAge(int age) {
this.age = age;
}

public void show() {//发生方法重写/覆盖
super.show();//这里的super指向父类对象,通过super就可以调用父类的成员
System.out.println("我是儿子,name=" + super.getName() + ",age=" + age);
}

}

注意:

当子类出现同名成员时,可以使用super来表明调用的是父类的成员

super的追溯并不仅限于父类,还可以是祖父类...

super的用法和this很像,但是this表示的是本类对象的引用,super表示的是父类对象的引用

(2) super 关键字调用父类构造函数

1) 子类在构造创建对象的时候,会先隐式调用父类的默认构造函数(相当于是 super()),创建父类对象之后才去创建子类对象。

2) 当父类没有默认构造函数的时候,子类构造方法中必须通过 this(参数) 或 super(参数) 调用本类或父类的构造函数,只能二选一。

3) 如果父类没有默认构造函数,子类也没有显式通过 super 关键字调用父类构造函数,会编译错误。

示例:

package com.edu.sup;

public class SuperDemo2 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Dog dog = new Dog();
}

}

class Animal { //父类
private String name;

    /*
* 解决办法1:
public Animal() {
// TODO Auto-generated constructor stub
}
*/
public Animal(String name) {//一个类如果写了构造函数,那么系统就不会添加默认构造函数
this.name = name;
}

    public String getName() {
return name;
}

    public void setName(String name) {
this.name = name;
}

public void show() {
System.out.println("我是父类,name=" + this.name);
}
}

class Dog extends Animal {
public Dog() {
/**
* 子类构造函数在构造子类对象的时候,会隐式去调用父类的默认构造函数,相当于调用 super(),但是由于父类 Animal 已经写了带参
* 构造函数,系统就不会给给它添加默认构造函数,子类 Dog 在隐式调用父类默认构造函数时就会因为找不到报编译错误。
* 解决办法:
* 1. 给父类 Animal 手动添加一个默认构造函数
* 2. 在子类 Dog 的构造函数中显式通过 super 调用父类的带参构造函数 
*/
//解决办法2:
//super("汪汪");
}
}

(3) 子类实例化的过程

package com.edu.ins;

public class InstanceDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
new Wolf();//匿名对象
}

}

class Creature {//生物类
public Creature() {
// TODO Auto-generated constructor stub
System.out.println("Creature 的无参构造函数");
}
}

class Animal extends Creature {//动物类,继承于 Creature 类
public Animal(String name) {
// TODO Auto-generated constructor stub
System.out.println("Animal 带一个参数的构造函数,name=" + name);
}

public Animal(String name, int age) {
// TODO Auto-generated constructor stub
this(name);//调用上面的构造函数
System.out.println("Animal 带两个参数的构造函数,name=" + name + ",age=" + age);
}
}

class Wolf extends Animal {//狼类,继承于 Animal 类
public Wolf() {
// TODO Auto-generated constructor stub
/**
* 子类构造函数在构造子类对象的时候,会隐式去调用父类的默认构造函数,相当于调用 super(),但是由于父类 Animal 已经写了带参
* 构造函数,系统就不会给给它添加默认构造函数,子类 Wolf 在隐式调用父类默认构造函数时就会因为找不到报编译错误。
* 解决办法:
* 1. 给父类 Animal 手动添加一个默认构造函数
* 2. 在子类 Wolf 的构造函数中显式通过 super 调用父类的带参构造函数
*/
super("灰太狼", 2);
System.out.println("Wolf 的无参构造函数");
}
}

从上面的执行结果可知,子类对象在实例化的时候(产生对象),会先调用父类构造函数产生父类对象之后再创建子类对象。

8. 面向对象编程的第三大基本特征:多态性

(1) 多态性

当父类引用指向子类对象的时候,对方法的调用是在程序的运行期间决定的,具体绑定的是哪一个子类对象,就调用哪一个子类对象的方法,这称为动态绑定(运行期绑定),多态性是建立在继承和方法覆盖的基础之上的,没有这个基础,就没有多态性的说法。

(2) 多态的语法:

父类 引用变量 = new 子类构造器();

引用变量.方法();

(3) 方法覆盖:

如果派生类(子类)中定义的一个方法,其方法名,返回类型和参数都和基类(父类)中某个方法一致,称为派生类覆盖了(重写了)基类 的方法。覆盖方法必须满足多种约束:

1) 派生类的方法名,参数和返回类型和基类一致

2) 基类的静态方法不能被派生类覆盖为非静态方法

3) 基类的非静态方法不能被派生类覆盖为静态方法

(4) 多态性的示例

package com.edu.dt;

public class DuoTaiDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
/**
* 当父类引用指向子类对象的时候,对方法的调用是在程序运行期决定的,具体绑定的是哪一个对象,就调用哪一个对象的方法,
* 这就是多态性。
*/
Animal a = new Dog();//目前父类引用a绑定的是Dog 对象
a.eat();

a = new Cat();//目前父类引用a绑定的是Cat 对象
a.eat();
//同一个引用变量a,调用同一个方法eat(),出现了不同的形态,一会儿是狗,一会儿是猫,原因是它绑定了不同的对象,这就是多态性
}
}

}

class Animal {//父类
public void eat() {
System.out.println("动物进食");
}
}

class Dog extends Animal {//子类1:狗类
public void eat() {//重写父类方法
System.out.println("狗啃骨头");
}
}

class Cat extends Animal {//子类2:猫类
public void eat() {//重写父类方法
System.out.println("猫吃鱼");
}
}

多态性的应用场景:多态性对于项目后期的维护和扩展性都非常有利,所以我们在写代码的时候,要尽量合理的使用多态性,比如:学校安装打印机:

package com.edu.dt;

public class PrinterDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//产生一个School对象
School school = new School();
Printer p = new BlackWhitePrinter();
school.setup(p);//此时p指向 BlackWhitePrinter 子类对象,调用的就是该子类的 print() 方法,是多态性
school.setup(new ColorPrinter());//此时参数指向的是 ColorPrinter 子类对象,多态性
}

}

class Printer {//打印机基类
public void print() {
System.out.println("打印机打印");
}
}

class BlackWhitePrinter extends Printer {//派生类:黑白打印机
public void print() { //方法覆盖
System.out.println("黑白打印机打印");
}
}

class ColorPrinter extends Printer {//派生类:彩色打印机
public void print() { //方法覆盖
System.out.println("彩色打印机打印");
}
}

//学校类
class School {
//装备打印机
/**

* @param printer:将父类引用作为形式参数,那么就可以使用子类对象作为实际参数给其赋值,那么就可以使用多态性
*/
public void setup(Printer printer) {
printer.print();//多态
}
}

后期我们要扩展一个打印机也非常的方便,只需要写一个打印机类继承父类即可,然后学校的准备打印机方法不变,利用多态性来调用该打印机的打印方法即可,同时代码的维护也比较方便。所以多态性有利用应用的维护和扩展,大家在以后的编程中要多使用多态性。

(5) 对象转型

1) 子类对象赋予父类引用的行为称为“向上转型”,上例的多态性就是属于该情况,”向上转型“是安全的
2) 父类对象赋予子类引用的行为称为“向下转型”,需要强制类型转换,不安全。

向上转型:Animal a = new Cat(); Creature c = a;//子类对象赋予父类引用

向下转型:Dog dog = (Dog)a;//父类对象赋予子类引用,需要强转,向下转型不安全,因为你不知道 a 指向的是 Cat 还是 Dog。

(6) instanceof 操作符:检测某个对象是否是某个类的实例

示例:

9. 里氏代换原则

里氏代换原则的核心思想是:子类必须能够替换掉它们的父类,并且保证系统行为不发生改变。换句话说,继承关系中的子类必须完全遵守父类的契约,不能违背父类的行为预期。

这一原则表面上看似简单,但在实际开发中,常常被忽略甚至误解,导致代码可读性差、维护成本高、扩展性低。

(1) 案例分析

我们通过反例和正例来直观理解这一原则。

1) 反例:不符哦里氏代换原则的设计

// 父类 Bird
public class Bird {
//父类定义的通用行为:假设所有鸟类都能飞。 
public void fly() {
System.out.println("Flying...");
}
}

// 子类 Penguin -继承Bird 类
public class Penguin extends Bird {
/**
* 重写父类的 fly 方法,但抛出了异常。
*/
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}

// 测试代码
public class Main {
public static void main(String[] args) {
Bird bird = new Bird();
bird.fly(); // 输出:Flying...

        Bird penguin = new Penguin();
// 输出:抛出异常:UnsupportedOperationException: Penguins can't fly!
penguin.fly();
}
}

问题分析:

1. 行为不一致

父类Bird假设所有鸟类都可以飞,但企鹅重写了fly方法并抛出异常,破坏了行为一致性。

2.违反多态性

调用者无法保证Bird类型变量的fly方法正常运行,破坏了多态的可靠性。

3.设计问题

将“飞行”强行定义在Bird类中,使得企鹅继承了不适合自身特性的功能。

2) 正例:符合里氏代换原则的设计

为了解决上述问题,重新设计父类 Bird。

// 抽象类 Bird
public abstract class Bird {
/**
* 定义鸟类的通用行为接口:移动方式
* 子类需按自身特性实现移动方式(飞行、游泳等)。
*/
public abstract void move();
}

// 会飞的鸟类 - FlyingBird
public class FlyingBird extends Bird {
/**
* 实现通用的移动行为:飞行
* FlyingBird 表示所有能飞的鸟类。
*/
@Override
public void move() {
System.out.println("Flying...");
}
}

// 企鹅类 - Penguin
public class Penguin extends Bird {
/**
* 实现通用的移动行为:游泳
* Penguin 表示不会飞的鸟类,但具备游泳能力。
*/
@Override
public void move() {
System.out.println("Swimming...");
}
}

// 测试代码
public class Main {
public static void main(String[] args) {
// 会飞的鸟类实例
Bird sparrow = new FlyingBird();
sparrow.move(); // 输出: Flying...

        // 企鹅类实例
Bird penguin = new Penguin();
penguin.move(); // 输出: Swimming...
}
}

改进后的优势:

1.遵循里氏代换原则

子类FlyingBird和Penguin都实现了父类的move方法,行为符合父类预期。

2.提高扩展性

新增鸟类(如鸵鸟)时,只需实现其独特的move方法,无需修改现有代码。

3.设计更合理

将父类Bird的抽象行为设计为具有通用性的move()方法,避免了不会飞的鸟类继承不适合的行为。

4.减少错误风险

子类行为始终满足父类预期,避免运行时错误。

改进后的设计不仅符合里氏代换原则,还通过抽象和具体实现分离,优化了继承关系,也避免了行为冲突导致的多态失效。

(2) 里氏代换原则的价值

1.增强系统稳定性

遵循里氏代换原则可确保继承体系内子类和父类行为一致,避免因行为冲突导致的运行时异常。

2.提升代码扩展性

子类在不修改父类的情况下可实现新功能,符合开闭原则,让系统更加灵活。

(3) 适用场景

1.类继承设计

在构建继承体系时,确保子类不会破坏父类的行为逻辑。例如,在设计公共服务类时,子类应保持接口一致,避免行为冲突。

2.多态场景

多态的核心是“父类引用指向子类实例”,而里氏代换原则是多态实现的基础。例如,Shape类的子类(如Circle和Rectangle)必须遵守Shape的行为规范。

3.接口设计与模块交互

在模块化开发中,遵守里氏代换原则可确保模块间接口替换时不会影响系统功能。例如,微服务架构中的服务需遵循接口规范。

(4) 总结

遵守里氏代换原则,不仅能避免继承体系中常见的设计陷阱,更能大幅提升代码的可扩展性和稳定性。

10. Object 类

(1) Object 类是所有 Java 类的根父类,如果某个类没有继承任何类,那么该类默认就继承于 Object 类。

例如:

public class Person {

}
//等效于:
public class Person extends Object {

}

(2) Object 类的主要方法

1. public Object():默认构造函数

2. public boolean equals(Object obj):默认情况下用于比较两个对象是否指向同一个内存地址

我们查看equals()方法的源码:

public boolean equals(Object obj) {
return (this == obj);
}
//可以发现,Object 类的 equals() 方法内部使用"=="来比较,所以它是比较两个对象是否指向同一个内存地址,但是这个 equals() 方法一般都会在子类中重写/覆盖

3. public int hashCode():取得 hash 码,一般用于子类重写

4. public String toString():打印类的属性值,一般用于子类重写

源码:

package com.edu.obj;

public class EqualsDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Person p1 = new Person();
Person p2 = new Person();
/**
* 这里的 Person 默认继承于 Object,那么这里调用的 equals() 方法就是 Object 类没有被重写的方法,内部是使用"=="来
* 比较两个对象是否指向同一个内存空间,但是 p1 和 p2 都是 new 出来,肯定指向不同的内存空间,所以这里返回false。
*/
System.out.println(p1.equals(p2));//false

String s1 = new String("您好,中国");
String s2 = new String("您好,中国");
/**
* String 类已经重写了 Object 类的 equals() 方法,我们查看 String 类的 equals() 方法,发现它是在比较两个字符串的内容
* 是否相等,虽然s1 和 s2 都是 new 出来的,但是它们的内容相等的,所以这里返回true,其中像 File、String、Date以及所有的
* 包装类都已经重写了 equals() 方法,都是比较内容,不是比较地址
*/
System.out.println(s1.equals(s2));//true
}

}

class Person {//Person 没有继承任何类,那么默认继承于 Object
private String name;
private int age;

public Person() {
// TODO Auto-generated constructor stub
}

    public Person(String name, int age) {
this.name = name;
this.age = 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;
}
}

针对上面这个例子,如果想让 p1 和 p2 对象比较内容是否相等,那么我们可以重写 equals() 方法,当然,这个不用我们自己写,直接点 eclipse 的 source 菜单的 Generate hashCode and equals()... 功能,eclipse 就能帮我们自己重写,注意,重写equals() 的时候一般都要重写 hashCode:

package com.edu.obj;

import java.util.Objects;

public class EqualsDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Person p1 = new Person("菲菲", 21);
Person p2 = new Person("菲菲", 21);
//Person 类重写后的 equals() 方法是用于比较属性值是否相等(即内容是否相等,而不是比较地址是否相等)
System.out.println(p1.equals(p2));//true

String s1 = new String("您好,中国");
String s2 = new String("您好,中国");
/**
* String 类已经重写了 Object 类的 equals() 方法,我们查看 String 类的 equals() 方法,发现它是在比较两个字符串的内容
* 是否相等,虽然s1 和 s2 都是 new 出来的,但是它们的内容相等的,所以这里返回true,其中像 File、String、Date以及所有的
* 包装类都已经重写了 equals() 方法,都是比较内容,不是比较地址
*/
System.out.println(s1.equals(s2));//true
}

}

class Person {//Person 没有继承任何类,那么默认继承于 Object
private String name;
private int age;

public Person() {
// TODO Auto-generated constructor stub
}

    public Person(String name, int age) {
this.name = name;
this.age = 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;
}

    @Override
public int hashCode() {
return Objects.hash(age, name);
}

    @Override
public boolean equals(Object obj) {
if (this == obj)//如果当前对象和传进来的obj指向同一个内存地址,那么它们的内容肯定相等
return true;
if (obj == null)//如果传进来的obj为null,直接返回false
return false;
if (getClass() != obj.getClass())//如果当前类的类型和传进来的obj的类型不相等,直接返回false
return false;
Person other = (Person) obj;//否则obj肯定是Person类型,向下转型为 Person 对象 other
//比较基本类型age是否相等并且比较字符串name的内容是否相等
return age == other.age && Objects.equals(name, other.name);
}


}

示例2:toString() 方法

Object 类默认的 toString() 方法用于输出该类的 hash 值,一般情况下我们也需要重写 toString() 方法,重写后的 toString() 方法用于输出各个属性的值,也可以通过 eclipse 的 Source 菜单来生成 toString() 方法:

package com.edu.obj;

import java.util.Objects;

public class EqualsDemo {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Person p1 = new Person("菲菲", 21);
Person p2 = new Person("菲菲", 21);
//Person 类重写后的 equals() 方法是用于比较属性值是否相等(即内容是否相等,而不是比较地址是否相等)
System.out.println(p1.equals(p2));//true
System.out.println(p1.toString());
System.out.println(p2);//直接输出对象时,系统会自动给其加上toString()方法,相当于执行p2.toString()

        String s1 = new String("您好,中国");
String s2 = new String("您好,中国");
/**
* String 类已经重写了 Object 类的 equals() 方法,我们查看 String 类的 equals() 方法,发现它是在比较两个字符串的内容
* 是否相等,虽然s1 和 s2 都是 new 出来的,但是它们的内容相等的,所以这里返回true,其中像 File、String、Date以及所有的
* 包装类都已经重写了 equals() 方法,都是比较内容,不是比较地址
*/
System.out.println(s1.equals(s2));//true
}

}

class Person {//Person 没有继承任何类,那么默认继承于 Object
private String name;
private int age;

public Person() {
// TODO Auto-generated constructor stub
}

    public Person(String name, int age) {
this.name = name;
this.age = 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;
}

    @Override
public int hashCode() {
return Objects.hash(age, name);
}

    @Override
public boolean equals(Object obj) {
if (this == obj)//如果当前对象和传进来的obj指向同一个内存地址,那么它们的内容肯定相等
return true;
if (obj == null)//如果传进来的obj为null,直接返回false
return false;
if (getClass() != obj.getClass())//如果当前类的类型和传进来的obj的类型不相等,直接返回false
return false;
Person other = (Person) obj;//否则obj肯定是Person类型,向下转型为 Person 对象 other
//比较基本类型age是否相等并且比较字符串name的内容是否相等
return age == other.age && Objects.equals(name, other.name);
}

    @Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}

11. 包装类的使用

(1) 针对于八种基本数据类型在 Java 中都有一个引用类型的类,称为包装类

有了包装类之后,就可以调用包装类的方法,这样 Java 真正实现了完全面向对象。

(2) 包装类的使用

1) 基本数据类型包装成包装类类型---装箱

a. 通过包装类的构造器实现
b. 通过字符串参数构造包装类对象

示例:

package com.edu.wrapper;

public class WrapperDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//1. 通过包装类的构造器把基本类型包装为包装类类型---装箱
int i = 10000;
Integer integer = new Integer(i);//把整型i包装为 Integer 对象
System.out.println(integer);

//2. 通过传递字符串给包装类构造器创建包装类对象
Float f = new Float("3.1415926");//注意:这个字符串参数只能是对应的数字类型,不能是像"abc"这样的字符串,否则会转换异常
System.out.println(f);
}

}

出错的情况:

2) 将包装类类型转换为基本数据类型---拆箱

a. 调用包装类的 xxxValue() 方法

package com.edu.wrapper;

public class WrapperDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//1. 通过包装类的构造器把基本类型包装为包装类类型---装箱
int i = 10000;
Integer integer = new Integer(i);//把整型i包装为 Integer 对象
System.out.println(integer);

//2. 通过传递字符串给包装类构造器创建包装类对象
Float f = new Float("3.1415926");//注意:这个字符串参数只能是对应的数字类型,不能是像"abc"这样的字符串,否则会转换异常
System.out.println(f);

//拆箱:将包装类类型转换为基本类型,xxxValue() 方法,xxx 对应各个基本类型:
int intValue = integer.intValue();
float floatValue = f.floatValue();
/**
* 注意:在 JDK5 之后支持自动装箱和拆箱 
*/
//直接将一个基本类型 long 的数据赋值给包装类 Long,实现了自动装箱,相当于执行了:Long l = new Long(200L);
Long l = 200L;
//自动拆箱:将包装类对象直接赋值给基本类型的变量,相当于执行:int mi = new Integer(200).intValue();
int mi = new Integer(200);
}

}

3) 字符串转换为基本数据类型

a. 通过包装类的构造器实现
b. 通过包装类的 parseXXX() 方法实现

package com.edu.wrapper;

public class WrapperDemo2 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//字符串转基本类型
//1. 通过包装类构造器实现
int i = new Integer("20000");//自动拆箱
System.out.println(i);
//2. 通过包装类的 parseXXX() 方法
double d = Double.parseDouble("3.1415926");
System.out.println(d);
}

}

4) 基本数据类型转字符串

a. 将基本类型的变量+空字符串“”
b. 通过 String 的静态方法 valueOf() 实现

示例:

package com.edu.wrapper;

public class WrapperDemo3 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//基本类型转字符串:
//1. 将基本类型变量 + ""
String s = 3.1415926 + "";
System.out.println(s);
//2. 通过 String 的 valueOf() 方法
String str = String.valueOf(20000);//String.valueOf() 方法是一个重载方法,可以处理各种数据类型
System.out.println(str);
}

}

课堂练习:完成八种基本数据类型的装箱和拆箱过程,并将字符串转换成基本数据类型,然后将基本数据类型转换成字符串。

12. Random 随机数类

(1) Random的两个构造方法

Random();

Random 类生成的随机数其实是伪随机,即就是有规律的随机数。

无参构造的话,以当前系统时间为种子,随机生成数字,由于将系统时间作为种子值,每次生成的随机数都不相同

Random(long seed);

有参构造的话,参数是一个种子数。给定一个种子数,其生成的随机数不管生成多少次,它的随机数都是一样的。

(2) Random类中的 nextInt(int bound) 方法;

该方法的参数是随机数生成的区间(约束),例如nextInt(10),[0,10)。包括0不包括10。

(3) Random类中的方法介绍

public boolean nextBoolean()

该方法的作用是生成一个随机的boolean值,生成true和false的值几率相等,也就是都是50%的几率。

public double nextDouble()

该方法的作用是生成一个随机的double值,数值介于[0,1.0)之间,也就是0到1之间的随机小数,包含0而不包含1.0。

public int nextInt()

该方法的作用是生成一个随机的int值,该值介于int的区间,也就是-231到231-1之间。

public int nextInt(int n)

该方法的作用是生成一个随机的int值,该值介于[0,n)的区间,也就是0到n之间的随机int值,包含0而不包含n。

public void setSeed(long seed)

该方法的作用是重新设置Random对象中的种子数。设置完种子数以后的Random对象和相同种子数使用new关键字创建出的Random对象相同。

示例1:

package com.edu.utils;

import java.util.Random;

public class RandomDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//以系统时间(毫秒记)作为种子值
Random r = new Random();
for (int i = 0; i < 10; i++) {
int num = r.nextInt(10) + 1;//生成[1,11),即1~10
System.out.print(num + " ");
}
}

}

//每次生成的随机数都不一样

示例2:

package com.edu.utils;

import java.util.Random;

public class RandomDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
//以系统时间(毫秒记)作为种子值
Random r = new Random();
for (int i = 0; i < 10; i++) {
int num = r.nextInt(10) + 1;//生成[1,11),即1~10
System.out.print(num + " ");
}
}

}

每次生成的随机数相同

13. static 关键字:静态的,类似于 C 语言中的全局变量

(1) static 的数据

有时候我们希望无论产生多少个对象,某些特定的数据在内存空间中只有一份,我们将这种内存空间中的只有一份的数据称为静态数据(static),即多个对象都共享这个 static 的数据,比如多个中国人都共享中国这个国家。

示例:

package com.edu.st;

public class StaticDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
/**
* 通过 new 操作符创建 p1,p2 对象时,JVM 会给p1 和 p2 分配不同的堆内存,p1 和 p2 拥有各自的 name 和 age 属性,互不干扰,
* 但是 count 属性是 static 的静态数据,在静态存储区中只有一份,被所有对象共享,即p1和p2对象都共享这个count,所以p1和p2
* 看到的count都是2,这种static类型的变量称为类变量,它归属于某个类,不归属于某个对象,被所有对象所共享。
*/
Person p1 = new Person("张三", 21);
Person p2 = new Person("李四", 22);
System.out.println(p1.count);//2
System.out.println(p2.count);//2
}

}

class Person {
private String name;
private int age;

public static int count = 0;//静态变量(类变量)

public Person() {
// TODO Auto-generated constructor stub
}

    public Person(String name, int age) {
count++;//静态变量自增
this.name = name;
this.age = 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;
}
}

对比一下实例变量:没有被 static 修饰的变量为实例变量,它归属于某个实例(对象)

package com.edu.st;

public class StaticDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Person p1 = new Person("张三", 21);
Person p2 = new Person("李四", 22);
//count为实例变量后,p1的count和p2的count互不干扰,各自拥有各自的,所以这里输出也是各自输出各自的1
System.out.println(p1.count);//1
System.out.println(p2.count);//1
}

}

class Person {
private String name;
private int age;

public int count = 0;//实例变量:归属于某个实例(对象)

public Person() {
// TODO Auto-generated constructor stub
}

    public Person(String name, int age) {
count++;//实例变量自增
this.name = name;
this.age = 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;
}
}

所以从上可知,static 类型的变量不归属于某个实例,被所有实例所共享,static 的成员变量称为类变量,static 的方法称为类方法,都归属于某个类,不归属于某个实例。而实例变量(没有被static修饰的变量)归属于某个实例,多个实例各自拥有各自的存储空间,各个实例变量归属于各自的实例,不会相互干扰。s

(2) 类属性/类变量(被 static 修饰的属性)和类方法(被 static 修饰的方法)的设计要素

1) 类属性作为该类各个对象所共享的变量,在设计类的时候,分析哪些属性不因对象的不同而改变,可以将这些属性设计为类属性(static),对应的方法设计成类方法(static)

2) 如果方法与调用者无关,可以将方法设计成类方法,类方法在调用的时候不用创建实例就可以直接通过“类名.类方法名()”就可以调用,当然,类属性也可以通过”类名.类属性“调用。因为static的成员不归属于某个对象,而是归属于某个类。

示例:

package com.edu.st;

public class StaticDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Person p1 = new Person("张三", 21);
Person p2 = new Person("李四", 22);
//count作为static的成员,被所有的实例共享
System.out.println(p1.count);//2
System.out.println(p2.count);//2
System.out.println(p1.getCount());//通过对象调用静态方法
System.out.println(p2.getCount());
System.out.println(Person.count);//通过"类名.类属性"调用类属性
System.out.println(Person.getCount());//通过"类名.类方法名()"调用类方法
}

}

class Person {
private String name;
private int age;

public static int count = 0;//类属性

public Person() {
// TODO Auto-generated constructor stub
}

    public Person(String name, int age) {
count++;//实例变量自增
this.name = name;
this.age = 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;
}

    public static int getCount() {
return count;
}

    public static void setCount(int count) {
Person.count = count;
}


}

(3) static 关键字可以使用的位置

1) static 可以修饰属性(成员变量)、方法、代码块、内部类

2) 被 static 修饰的成员有以下特点:

a. 随着类的加载而加载(注意:Java 的一个类首先需要被虚拟机加载之后才能运行,类的加载发生在对象产生之前)
b. 先于对象存在(静态成员的初始化先于实例成员的初始化)
c. 用于修饰属性的时候被所有对象所共享
d. 可以不用创建对象,直接通过类名来调用
e. 由于 static 的成员是在类加载的时候初始化的,是先于对象存在的,所以在 static 的成员方法中或代码块中不能使用 this 或 super。

示例:静态成员和示例成员的初始化顺序

示例2:当然,static的方法中也不能使用 this 或 super

示例3:静态成员的内存结构图

package com.edu.st;

public class StaticDemo3 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Chinese.nation = "中国";
Chinese chinese1 = new Chinese();
chinese1.name = "张三";
chinese1.age = 20;

Chinese chinese2 = new Chinese();
chinese2.name = "李四";
chinese2.age = 21;
}

}
class Chinese{
String name;//实例变量
int age;
static String nation;//静态变量/类变量
}

(4) 通过 static 关键字总结出来的设计模式:单例设计模式

1) 设计模式:

在大量的编程实践中总结和理论优化后优选的代码结构,编程风格,以及解决问题的思路,设计模式免去了我们自己再思考和摸索,就像经典的棋谱,不同的棋局,用不同的棋谱。

2) 单例设计模式(SingleTon)

就是采用一定的方法保证在整个软件系统中,对某个类只存在一个实例,并且该类值提供一个获取该实例的方法。

3) 单例设计模式分为两种:饿汉式和懒汉式

a. 饿汉式:提前准备好实例

b. 懒汉式:不提前准备实例,用的时候才创建,懒汉式可能存在线程安全问题

(5) Java 中的 main 方法

package com.edu.st;

public class MainDemo {
/**
* 由于 Java 的 main 方法被设计成 static 的,所以Java虚拟机在调用main 方法的时候,不用去产生对象直接通过类名就可以调用,
* 但是 static 的方法中是不能直接访问实例成员,因为 static 成员的初始化是先于对象的产生,解决办法就是在 main 方法中先产生
* 对象,再通过对象访问。
*/
private int a;//实例变量

public static void main(String[] args) {
// TODO Auto-generated method stub
//a = 10000;
MainDemo md = new MainDemo();//产生对象后,通过对象访问实例成员就没问题了
md.a = 10000;

//然后就是main方法的参数(String[] args)是控制台传递给 main 方法的运行参数,是一个String 的数组
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "]=" + args[i]);
}
}

}

使用命令行编译,设置命令行参数:

(6) 代码块:{}

1) 作用:完成初始化

2) 分类:

a. 不被 static 修饰的代码块,称为代码块,用于始化实例成员,特点如下初:
a) 可以有输出语句
b) 可以初始化实例变量
c) 可以初始化静态变量
d) 如果有多个这样的代码亏,按照定义的先后顺序执行
e) 每次创建对象的时候,都会执行一次,且先于构造函数执行
b. 被 static 修饰的代码块,称为静态代码块,用于初始化静态成员,特点如下:
a) 可以包含输出语句
b) 可以初始化静态变量
c) 可以调用静态方法
d) 如果有多个静态代码块,也是按照定义先后顺序执行
e) 静态代码块是先于代码块执行的,原因是静态代码块的执行发生在类加载的时候,是先于对象的产生
f) 静态代码块是在类加载的时候执行的,且只会执行一次

示例:不被 static 修饰的代码块

package com.edu.st;

public class CodeBlockDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student();
s1.show();

Student s2 = new Student();
s2.show();
/**
* 创建了两个对象,代码块就执行了两次,并且是在构造函数执行之前执行,多个代码的执行顺序是按照定义的先后执行的
*/
}

}

class Student {
private String name;//实例变量
private static int count;//静态变量/类变量

public Student() {
// TODO Auto-generated constructor stub
System.out.println("Student 的构造函数");
}
//没被 static 修饰的代码块
{
/**
*可以有输出语句
可以初始化实例变量
可以初始化静态变量
如果有多个这样的代码,按照定义的先后顺序执行
每次创建对象的时候,都会执行一次,且先于构造函数执行
*/
name = "张三";//初始化实例变量
count = 20;//初始化静态变量
System.out.println("我是代码块1");
}
{
System.out.println("我是代码块2");
System.out.println(count);
}
public void show() { //实例方法
System.out.println("我是学生,name=" + name);
}
}

示例2:静态代码块:

package com.edu.st;

public class CodeBlockDemo1 {

    public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student();
s1.show();

Student s2 = new Student();
s2.show();
/**
* 创建了两个对象,代码块就执行了两次,并且是在构造函数执行之前执行,多个代码的执行顺序是按照定义的先后执行的
*/
}

}

class Student {
private String name;//实例变量
private static int count;//静态变量/类变量

public Student() {
// TODO Auto-generated constructor stub
System.out.println("Student 的构造函数");
}
//没被 static 修饰的代码块
{
/**
*可以有输出语句
可以初始化实例变量
可以初始化静态变量
如果有多个这样的代码亏,按照定义的先后顺序执行
每次创建对象的时候,都会执行一次,且先于构造函数执行
*/
name = "张三";//初始化实例变量
count = 20;//初始化静态变量
System.out.println("我是代码块1");
}
{
System.out.println("我是代码块2");
System.out.println(count);
}
static {
/**
可以包含输出语句
可以初始化静态变量
可以调用静态方法
如果有多个静态代码块,也是按照定义先后顺序执行
静态代码块是先于代码块执行的,原因是静态代码块的执行发生在类加载的时候,是先于对象的产生
静态代码块是在类加载的时候执行的,且只会执行一次
*/
//企图在静态代码块中访问实例成员,会出现编译错误,同理,也不能使用this 或 super
//name = "李四";
//this.name = "娜娜";
count = 1000;//可以访问静态成员
System.out.println("我是静态代码块");
}
public void show() { //实例方法
System.out.println("我是学生,name=" + name);
}
}

14. final 关键字:类似于 C 语言的 const 关键字,表示不可变的

(1) 在 Java 中声明类、变量、方法的时候都可以加上 final ,有不变的意思

1) 被 final 修饰的类不能够被继承

其中 String 类、System 类以及 StringBuffer 类都是 final 类,不能够被继承。

2) 被 final 修饰的方法不能被重写

3) 被 final 修饰的变量表示常量,不能被修改,常量一般大写

示例4:final 修饰的变量,不能被修改,但是 final 修饰的引用类型变量,表示不能改变该引用变量指向的地址,但是可以改它里面的值。

示例5:final参数:基本类型,引用类型

http://www.dtcms.com/a/279689.html

相关文章:

  • JAR 包冲突排雷指南:原理、现象与 Maven 一站式解决
  • 深度解读virtio:Linux IO虚拟化核心机制
  • 评论设计开发
  • RedisJSON 技术揭秘`JSON.DEBUG MEMORY` 量化 JSON 键的内存占用
  • Python深浅拷贝全解析:从原理到实战的避坑指南
  • 深度解析:htmlspecialchars 与 nl2br 结合使用的前后端协作之道,大学毕业论文——仙盟创梦IDE
  • 工业场合需要千变万化的模拟信号,如何获取?
  • B4016 树的直径
  • 阿尔卡特ASM180TD181TD氦检漏器ALCATEL
  • 使用dify生成测试用例
  • 【第一章编辑器开发基础第二节编辑器布局_3间距控制(4/4)】
  • OpenCV C++ 中的掩码(Mask)操作
  • 微服务初步入门
  • 设计模式之适配器模式:让不兼容的接口协同工作的艺术
  • Unreal5从入门到精通之如何实现UDP Socket通讯
  • 【C++进阶】---- 多态
  • 解锁文档处理新体验:Python库Agentic Document Extraction
  • OneCode3.0 通信架构简介——MCPServer微内核设计哲学与实现
  • Web学习笔记4
  • 算法训练营day16 513.找树左下角的值、112. 路径总和、106.从中序与后序遍历序列构造二叉树
  • 探索 Sort.h:多功能排序算法模板库
  • [element-ui]el-table在可视区域底部固定一个横向滚动条
  • 智源全面开源RoboBrain 2.0与RoboOS 2.0:刷新10项评测基准,多机协作加速群体智能
  • MCP 第三波升级!Function Call 多步调用 + 流式输出详解
  • QWidget 和 QML 的本质和使用上的区别
  • 慢查询日志监控:定位性能瓶颈的第一步
  • 【抖音滑动验证码风控分析】
  • 小架构step系列14:白盒集成测试原理
  • C# TCP粘包与拆包深度了解
  • spark广播表大小超过Spark默认的8GB限制