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参数:基本类型,引用类型