Java面向对象中级
包
【包的三大作用】
- 区分相同名字的类
- 当类很多时,可以很好管理类【API文档】
- 控制访问的范围
【包基本语法】
package com.yyy;
说明:
- package关键字,表示打包
- com.yyy ,表示包名
【包的本质(原理)】
包的本质实际是创建不同的文件夹/目录来保存类文件
【包的应用场景】:当开发同一个Java项目时,希望定义两个或者多个相同名字的类——包
【包的命名】
【包的引入】
【注意事项与细节】
- package的作用是声明当前类所在的包,需要放在class的最上面,一个类中最多只有一个package语句
- import指令 位置放在package的下面,在类定义的前面,可以有多句且没有顺序要求
【idea导包快捷键】
Alt+Enter
手动导入
访问修饰符
【基本介绍】
Java中提供了四种访问修饰符,用于控制方法和属性(成员变量)的访问权限(范围):
- 公开级别:用
public
修饰,对外公开 - 受保护级别:用
protected
修饰,对子类和同一个包中的类公开 - 默认级别(友好类型):
没有修饰符号
,向同一个包的类公开 - 私有级别:用
private
修饰,只有类本身可以访问,不对外公开
【那么什么情况该用什么修饰符呢?】
从作用域来看,public能够使用所有的情况。 但是大家在工作的时候,又不会真正全部都使用public,那么到底什么情况该用什么修饰符呢?
属性通常使用private封装起来
方法一般使用public用于被调用
会被子类继承的方法,通常使用protected
package用的不多,一般新手会用package,因为还不知道有修饰符这个东西
再就是作用范围最小原则
简单说,能用private就用private,不行就放大一级,用package,再不行就用protected,最后用public。 这样就能把数据尽量的封装起来,没有必要露出来的,就不用露出来了
封装(***)
封装(encapsulation)就是把抽象的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其它部分只能通过被授权的操作(方法)——get和set方法,才能对数据进行操作。
封装的好处:
- 隐藏细节:方法(连接数据库)<--- 调用(传入参数...)
- 可以对数据进行验证,保证合理安全(p.age = 1000 不合理)
实现封装三步骤
-
将属性进行私有化(private)【不能直接修改属性】
-
提供一个公共的(public)set方法,用于对属性判断并赋值
public void setXxx(类型 参数名) {// Xxx 表示某个属性
// 加入验证数据的业务逻辑
属性 = 参数名;
}
-
提供一个公共的(public)get方法,用于获取属性的值
public 数据类型 getXxx(){ // 权限判断,Xxx某个属性
return xx;
}
快捷键:Alt + insert
创建两个程序,在其中定义两个类:Account和AccountTest类体会Java的封装性
- Account类要求具有属性:姓名(长度为2~4位)、余额(必须>20)密码(密码必须为6位),如果不满足则给出提示信息,并给默认值
- 通过setXxx的方法给Account的属性赋值
- 在AccountTest类中测试
【参考代码】
// 类
public class Account {
// 为了体现封装 我们用privat
private String name;
private double salary;
private String passward;
// 属性私有 set get方法
/*
创建两个程序,在其中定义两个类:Account和AccountTest类体会Java的封装性
1. Account类要求具有属性:姓名(长度为2~4位)、余额(必须>20)密码(密码必须为6位),如果不满足则给出提示信息,并给默认值
2. 通过setXxx的方法给Account的属性赋值
3. 在AccountTest类中测试
*/
// 提供两个构造器
public Account() {
}
public Account(String name, double salary, String passward) {
this.setName(name);
this.setSalary(salary);
this.setPassward(passward);
}
public String getName() {
return name;
}
// //姓名(长度为2~4位)
public void setName(String name) {
if(name.length() >= 2 && name.length() <= 4){
this.name = name;
}else{
System.out.println("名字的长度为2~4位");
// 设置默认值
this.name = "无名人";
}
}
public double getSalary() {
return salary;
}
// 余额(必须>20)
public void setSalary(double salary) {
if(salary > 20.0){
this.salary = salary;
}else{
System.out.println("余额不能低于20闷");
this.salary = 0.0;
}
}
public String getPassward() {
return passward;
}
// 密码(密码必须为6位)
public void setPassward(String passward) {
if(passward.length() == 6){
this.passward = passward;
}else{
System.out.println("密码的长度必须为6位噢!默认为000000");
this.passward ="000000";
}
}
// 显示账号信息
public void printMsg(){
System.out.println("账号信息 name=" + name + "余额 salary=" + salary + "密码 passward=" + passward);
}
}
----------------------
测试类
public class AccountTest {
public static void main(String[] args) {
// Account test01 = new Account();
// test01.setName("小明");
// test01.setSalary(19.9);
// test01.setPassward("123456");
// 调用带有参数的构造器也可以 因为它里面我们自己写了set 方法
Account test01 = new Account("小明",60.0,"123456");
test01.printMsg();
}
}
继承(***)
为什么需要继承
我们编写了两个类,一个是Pupil类(小学生),一个是Graduate类(大学生),发现两个类的属性和方法有很多是相同的,代码重复性大,怎么办?——》 继承(解决代码复用性)
继承原理图
【继承基本介绍】
继承可以解决代码的复用。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只要通过extends
来声明继承父类即可。
【继承的基本语法】
class 子类 extends 父类 {
}
- 子类(派生类)会自动拥有父类(超类/基类)定义的属性和方法
继承的细节
- 子类继承了所有的属性和方法,非私有的属性和方法可以在子类直接访问,但是私有属性不能在子类直接访问,要通过父类提供的的方法去访问(在父类提供一个公共(public)方法)
package ExtendsDetail;
public class Base { // 父类
public int n1 = 100;
protected int n2 = 200;
int n3= 300;
private int n4 = 400;
// 构造方法
public Base(){
System.out.println("Base...");
}
// 父类提供一个public方法,返回n4
public int getN4() {
return n4;
}
public void test100(){
System.out.println("test100()...");
}
protected void test200(){
System.out.println("test200()...");
}
void test300(){
System.out.println("test300()...");
}
private void test400(){
System.out.println("test400()...");
}
//
public void callTest400() {
test400();
}
}
--------------子类
package ExtendsDetail;
public class Sub extends Base{// 子类
public Sub(){ // 子类构造器
System.out.println("Sub()...");
}
public void sayOk(){ // 子类方法
// 非私有的属性和方法可以在子类直接访问
System.out.println(n1 + " " + n2 + " " + n3); // n4 报错
test100();
test200();
test300();
// test400(); 报错
// 私有属性不能在子类直接访问,要通过父类提供的的方法去访问
System.out.println("n4= " + getN4());
callTest400(); // 调用test400() 中转
}
}
-------------测试主类
package ExtendsDetail;
public class ExtendsDetail01 {
public static void main(String[] args) {
Sub sub = new Sub();
sub.sayOk();
}
}
- 子类必须调用父类的构造器,完成父类的初始化(先调用父类后才调用子类)
package ExtendsDetail;
public class ExtendsDetail01 {
public static void main(String[] args) {
Sub sub1 = new Sub();// 创建第一个子类对象
System.out.println("-----------------");
Sub sub2 = new Sub("tom");// 创建第二个子类对象
}
}
-------------父类
package ExtendsDetail;
public class Base { // 父类
// 父类无参构造方法
public Base(){
System.out.println("父类无参构器Base()...");
}
}
-------------子类
package ExtendsDetail;
public class Sub extends Base{// 子类
// super(); 默认调用父类无参构造器
public Sub(){ // 子类构造器
System.out.println("子类构造器Sub()...");
}
//当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器
public Sub(String name){
System.out.println("子类构造器Sub(String name)被调用");
}
}
输出结果:
父类无参构器Base()...
子类构造器Sub()...
父类无参构器Base()... (不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器)
子类构造器Sub(String name)被调用
package ExtendsDetail;
public class ExtendsDetail01 {
public static void main(String[] args) {
Sub sub3 = new Sub("tom",18);
}
}
---------------父类
package ExtendsDetail;
public class Base { // 父类
// 父类无参构造方法
// public Base(){
// System.out.println("父类无参构器Base()...");
// }
// 我们自己定义了构造器原有无参构造器就被覆盖了,如想使用必须显示定义回来
public Base(String name, int age){
System.out.println("父类构器Base(String name, int age)被调用...");
}
}
---------------子类
package ExtendsDetail;
public class Sub extends Base{// 子类
/*
如果父类没有提供无参构造器,
则必须在子类构造器中用super去指定使用父类的哪个构造器完成对父类的初始化工作,
否则编译不会通过。
*/
public Sub(String name,int age){
super("tom",18);
System.out.println("子类构造器Sub(String name,int age)被调用...");
}
}
父类构器Base(String name, int age)被调用...
子类构造器Sub(String name,int age)被调用...
【总结】
当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类构造器中用super去指定使用父类的哪个构造器完成对父类的初始化工作,否则编译不会通过。(如果父类无参构造器被覆盖了,则必须在子类的每一个构造器中去指定到底要用父类的哪一个构造器)
- 如果希望指定去调用父类的某个构造器,则显示调用一下:super(参数列表)
package ExtendsDetail;
public class ExtendsDetail01 {
public static void main(String[] args) {
Sub sub3 = new Sub("tom",18);
}
}
--------------父类
package ExtendsDetail;
public class Base { // 父类
//父类无参构造方法
public Base(){
System.out.println("父类无参构器Base()被调用...");
}
public Base(String name){
System.out.println("父类无参构器Base(String name)被调用...");
}
public Base(String name, int age){
System.out.println("父类构器Base(String name, int age)被调用...");
}
}
----------------子类
package ExtendsDetail;
public class Sub extends Base{// 子类
// 如果希望指定去调用父类的某个构造器,则显示调用一下:super(参数列表)
public Sub(String name,int age){
//1. 我们想要调用父类的无参构造器 如下,或者什么都不写,默认就是super()
//super();
//2. 我们想要调用父类的Base(String name)构造器
//super("tom");
//3. 我们想要调用父类的Base(String name, int age)构造器
super("tom",18);
System.out.println("子类构造器Sub(String name,int age)被调用");
}
}
-
super在使用时,必须放在构造器的第一行(super只能在构造器中使用)(先有爸爸才有儿子)
-
super()和this()都只能放在构造器的第一行,因此这两个方法不能共存在一个构造器中
-
Java中Object类是所有类的父类
CTRL + h
可以看到类的继承关系
- 父类构造器的调用不限于直接父类!将一直往上追溯到Object类(顶级父类)
package ExtendsDetail;
public class ExtendsDetail01 {
public static void main(String[] args) {
Sub sub3 = new Sub("tom",18);
}
}
----------------TopBase祖父类
package ExtendsDetail;
public class TopBase { // 父类是Object类
public TopBase(){
System.out.println("TopBase()构造器被调用...");
}
}
----------------Base父类
package ExtendsDetail;
public class Base extends TopBase{ // 父类
//父类无参构造方法
public Base(){
System.out.println("父类无参构器Base()被调用...");
}
public Base(String name){
System.out.println("父类无参构器Base(String name)被调用...");
}
public Base(String name, int age){
// 这里有一个默认的super()
System.out.println("父类构器Base(String name, int age)被调用...");
}
}
--------------Sub子类
package ExtendsDetail;
public class Sub extends Base{// 子类
// 如果希望指定去调用父类的某个构造器,则显示调用一下:super(参数列表)
public Sub(String name,int age){
super("tom",18);
System.out.println("子类构造器Sub(String name,int age)被调用...");
}
}
输出结果:
TopBase()构造器被调用...
父类构器Base(String name, int age)被调用...
子类构造器Sub(String name,int age)被调用...
-
子类最多只能继承一个父类(直接继承的父类只能为一个),即Java中是单继承机制
如何让A类继承B类和C类?【A继承B,B继承C】——中专,间接继承
- 不能滥用继承,子类和父类之间必须满足 is - a 的逻辑关系
Person is music ?
Music extends Person // 不合理
Animal
Cat extends Animal // 合理
继承本质分析
当用son对象去访问Name,age,hobby时规则又是怎样的?
【查找时遵循如下查找关系】
(1)首先看子类是否有该属性
(2)如果子类有该属性,并且可以访问,则返回信息
属性私有在内存空间里还是存在的,但是不可以直接访问,如要访问就得提供公共public方法:
public class ExtendsTheory {
public static void main(String[] args) {
Son son = new Son(); // 内存的布局
System.out.println(son.name); // 大头儿子
// System.out.println(son.age); // 39
System.out.println(son.getAge());// 39
System.out.println(son.hobby); // 睡觉
}
}
class GrandPa { // 爷类
String name = "大头爷爷";
String hobby = "睡觉";
}
class Father extends GrandPa { // 父类
String name = "大头爸爸";
// int age = 39;
private int age = 39; // 属性私有在内存空间里还是存在的,但是不可以直接访问,如要访问就得提供公共public方法
public int getAge() {
return age;
}
}
class Son extends Father { // 子类
String name = "大头儿子";
}
(3)如果子类没有这个属性,就看父类有没有这个属性(如果父类有该属性,并且可以访问,则返回信息)
(4)如果父类没有就按(3)规则,继续找上级父类,直到Object...
【继承练习】——继承的细节
练习01:main中: B b = new B(); 会输出什么?
public class ExtendsTheory {
public static void main(String[] args) {
B b = new B();// 调用默认子类构造器 01
}
}
class A {
A() {// 04
System.out.println("a");// 05
}
A(String name) {
System.out.println("a name");
}
}
class B extends A {
B() { // 01
this("abc"); // 因为有了this调用构造器,有了this就不能有super了 02
System.out.println("b"); // 07
}
B(String name) { // 03
// super(); 默认有一个super() // 04
System.out.println("b name");// 06
}
}
输出结果:
a
b name
b
练习02:C c = new C(); 输出结果是什么?
public class ExtendsTheory {
public static void main(String[] args) {
C c = new C();// 无参构造器01
}
}
class A {
public A(){ // 06
System.out.println("我是A类"); // 07
}
}
class B extends A {
public B() {
System.out.println("我是B类的无参构造器");
}
public B(String name) { // 05
//super(); 注:这里有一个默认的super() // 06
System.out.println(name + "我是B类的有参构造器"); // 08
}
}
class C extends B{
public C() { // 01
this("hello"); // 02
System.out.println("我是C类的无参构造器"); // 10
}
public C(String name) {// 03
super("haha");// 04
System.out.println("我是C类的有参构造器"); // 09
}
}
输出结果:
我是A类
haha我是B类的有参构造器
我是C类的有参构造器
我是C类的无参构造器
练习03:
编写Computer类,包含CPU、内存、硬盘等属性,getDetails方法用于返回Computer的详细信息
编写PC子类,继承Computer类,添加特有属性【品牌brand】
编写NotePad子类,继承Computer类,添加特有属性【color】
编写Test类,在mian方法中创建PC和NotePad对象,分别给对象特有属性赋值,以及从Computer类继承的属性赋值,并使用方法打印输出信息
package ExtendsTest03;
/*
编写Computer类,包含CPU、内存、硬盘等属性,getDetails方法用于返回Computer的详细信息
*/
public class Computer {
private String CPU;
private int memory;
private int disk;
public Computer(String CPU, int memory, int disk) {
this.CPU = CPU;
this.memory = memory;
this.disk = disk;
}
public String getCPU() {
return CPU;
}
public void setCPU(String CPU) {
this.CPU = CPU;
}
public int getMemory() {
return memory;
}
public void setMemory(int memory) {
this.memory = memory;
}
public int getDisk() {
return disk;
}
public void setDisk(int disk) {
this.disk = disk;
}
// getDetails方法用于返回Computer的详细信息
public String getDetails() {
return "cpu= " + CPU + " " + "memory= " + memory + " " + "disk= " + disk;
}
}
----------
package ExtendsTest03;
/*
编写PC子类,继承Computer类,添加特有属性【品牌brand】
*/
public class PC extends Computer{
private String brand;
// 注:由于父类已经没有了默认的无参构造器,一开始就会报错
//这里idea直接根据继承的规则,自动把构造器调写好了
//这里体现出继承基本思想:父类的构造器完成父类的初始化,子类的构造器完成子类的初始化
public PC(String CPU, int memory, int disk, String brand) {
super(CPU, memory, disk);// 父类的构造器完成父类的初始化
this.brand = brand;// 子类的构造器完成子类的初始化
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
// 调用父类getDetails()方法
public void printMsg01(){
System.out.println("此PC信息:" + getDetails() + " " + "brand=" + brand);
}
}
------
package ExtendsTest03;
/*
编写NotePad子类,继承Computer类,添加特有属性【color】
*/
public class NotePad extends Computer{
private String color;
public NotePad(String CPU, int memory, int disk, String color) {
super(CPU, memory, disk);
this.color = color;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public void printMsg02() {
System.out.println("此NotePad信息:" + getDetails() + " " + "color=" + color);
}
}
-----
package ExtendsTest03;
/*
编写Test类,在mian方法中创建PC和NotePad对象,分别给对象特有属性赋值,以及从Computer类继承的属性赋值,并使用方法打印输出信息
*/
public class Test {
public static void main(String[] args) {
// 继承——减少代码复用性
PC pc = new PC("intel",16,256,"IMB");
pc.printMsg01();
NotePad notePad = new NotePad("intel",16,256,"红色");
notePad.printMsg02();
}
}
继承基本思想:父类的构造器完成父类的初始化,子类的构造器完成子类的初始化(代码52~58行)
super()
super代表父类的引用,用于访问父类的属性,方法,构造器
【基本语法】
-
访问父类的属性,但不能访问父类的private属性
super.属性名
-
访问父类的方法,但不能访问父类的private方法
super.方法名
-
访问父类构造器(前面的例子都有用过)
super(参数列表);只能放在构造器的第一句,只能出现一句
package Super;
public class A {
// 4个属性
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;
// 4个方法
public void test100(){
}
protected void test200(){
}
void test300(){
}
private void test400(){
}
// 父类构造器
public A(){}
public A(String name){}
public A(String name, int age){}
}
--------
package Super;
public class B extends A{
/*
访问父类的属性,但不能访问父类的private属性
super.属性名
*/
public void hi(){
System.out.println(super.n1 + super.n2 + super.n3);
}
/*
访问父类的方法,但不能访问父类的private方法
super.方法名
*/
public void ok() {
super.test100();
super.test200();
super.test300();
// super.test400(); 但不能访问父类的private方法
}
/*
访问父类构造器(前面的例子都有用过)
super(参数列表);只能放在构造器的第一句,只能出现一句
*/
public B(){
// super();
// super("tom");
super("tom",18);
}
}
【super使用细节】
- 调用父类构造器的好处(分工明确,父类属性由父类初始化,子类属性由子类初始化)
- 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的效果(继承的本质)
package Super;
public class A {
public void cal(){
System.out.println("A类中的cal()方法...");
}
}
------
package Super;
public class B extends A{
/*
如果没有重名,使用super、this、直接访问是一样的效果
*/
public void sum(){
System.out.println("B类中的sum()...");
//希望调用A类中的cal()方法
//这时,因为子类B中没有cal()方法,因此可以使用下面三种方式
//1.cal(); 找cal()方法时的顺序是
/*
1.先找本类,如果有则调用
2.如果没有,则找父类(如果有,并可以调用,则调用)
3.如果父类没有,则继续找父类的父类,直到Object
提示:
1.如果查找方法的过程中,找到了但不能访问,则报错
2.如果查找方法的过程中,没有找到,则提示方法不存在,报错
*/
//cal();
//this.cal();//2. 等价于cal()
super.cal(); //3.super.cal(); 找cal()方法的顺序是直接查找父类,其它规则一样
}
}
-----
package Super;
public class Super01 {
public static void main(String[] args) {
B b = new B();
b.sum();
}
}
如果子类中的属性或者方法没有与父类重名,使用super、this、直接访问是一样的效果因此可以使用下面三种方式
//1.cal(); 找cal()方法时的顺序是 /* 1.先找本类,如果有则调用 2.如果没有,则找父类(如果有,并可以调用,则调用) 3.如果父类没有,则继续找父类的父类,直到Object 提示: 1.如果查找方法的过程中,找到了但不能访问,则报错 2.如果查找方法的过程中,没有找到,则提示方法不存在,报错 */ //cal(); //this.cal();//2. 等价于cal() super.cal(); //3.super.cal(); 找cal()方法的顺序是直接查找父类,其它规则一样 属性也一样!
- super的访问不限于直接父类,如果爷爷类中和本类有同名,也可以使用super去访问爷爷类的成员;如果多个上级类中有同名的成员,使用super访问遵循就近原则,当然也要遵守访问权限的相关规则
package Super;
public class C {
public int n1 = 999;
public int age = 111;
public void cal(){
System.out.println("Base类中的cal()方法...");
}
public void eat(){
System.out.println("Base类中的eat()方法...");
}
}
-----
package Super;
public class A extends C{
public int n1 = 100;
public void cal(){
System.out.println("A类中的cal()方法...");
}
}
-----
package Super;
public class B extends A{
public int n1 = 888;
// 测试方法
public void test(){
/*
super的访问不限于直接父类,如果爷爷类中和本类有同名,也可以使用super去访问爷爷类的成员;
如果多个上级类中有同名的成员,使用super访问遵循就近原则
*/
System.out.println("super.n1=" + super.n1); // 100
// 如果A类中的public int n1 = 100;注释掉了 则结果访问的就是爷爷类中的public int n1 = 999;
super.cal();// A类和C类都有——就近原则:调用的是A类中的cal()
super.eat();// 直接父类A中没有,而C中有:调用的是C类中的eat()
}
}
-----
package Super;
public class Super01 {
public static void main(String[] args) {
B b = new B();
b.test();
}
}
【super和this比较】
No. | 区别点 | this | super |
---|---|---|---|
1 | 访问属性 | 先访问本类中的属性,如果本类中没有此属性,则从父类中继续查找 | 从父类开始查找属性 |
2 | 调用方法 | 先访问本类中的方法,如果本类中没有此方法,则从父类中继续查找 | 从父类开始查找方法 |
3 | 调用构造器 | 调用本类构造器,必须放在构造器首行 | 调用父类构造器,必须放在构造器首行 |
4 | 特殊 | 表示当前对象 | 子类中访问父类对象 |
方法重写/覆盖(override)
【基本介绍】
方法重写/覆盖就是子类有一个方法,和父类的某个方法的名称,返回类型,参数完全一致,那我们就说子类重写/覆盖了父类的方法
方法重写的目的:
子类通过方法的重写可以隐藏继承的方法。一旦子类重写了父类方法f,就隐藏了继承方法f,那么子类对象调用方法f一定是调用重写的方法f。如果子类想使用被隐藏的方法,就必须用super关键字。
package Override;
public class Animal {
public void cry() {
System.out.println("动物叫...");
}
}
-----
package Override;
public class Dog extends Animal{
// Dog的cry()方法 重写/覆盖了Animal的 cry()方法
public void cry(){
System.out.println("小狗汪汪叫...");
}
}
快捷键:alt + insert
【方法重写细节】
-
子类方法的参数,方法名称,要和父类方法的参数,方法名称完全一致(如果不一样则不会将父类方法进行覆盖)
-
子类方法的返回类型和父类返回类型一致,或者是父类返回类型的子类,比如父类是Object,子类的返回类型是String
-
子类方法不能缩小父类方法的访问权限——public > protected > 默认 > private
【方法重载与重写】
名称 | 发生范围 | 方法名 | 形参列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载(overload) | 本类 | 必须一样 | 类型,顺序或者个数不同 | 无要求 | 无要求 |
重写(override) | 父子类 | 必须一样 | 相同 | 子类重写的方法,返回的类型和父类返回的类型一致;或者是其子类 | 子类方法不能缩小父类方法的访问范围 |
练习:
- 编写一个Persom类,包括属性/private(name,age),构造器,方法say(返回自我介绍的字符串)
- 编写一个Student类,继承Person类,增加id、score属性/private,以及构造器,定义say方法(返回自我介绍的信息)
- 在main中分别创建Person对象和Student对象,调用say方法输出自我介绍
package Override;
public class Person {
private String name;
private int age;
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;
}
public String say(){
return "name=" + name + "age=" + age;
}
}
-----
package Override;
public class Student extends Person{
private int id;
private double score;
public Student(String name, int age, int id, double score) {
super(name, age);// 这里调用了父类构造器
this.id = id;
this.score = score;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
// 重写父类say方法
public String say(){
// 减少代码复用——super
return super.say() + "id=" + id + "score=" + score;
}
}
-----package Override;
public class Test {
public static void main(String[] args) {
Person p = new Person("tom",18);
System.out.println(p.say());
Student student = new Student("tom",18,2020123,99.9);
System.out.println(student.say());
}
}
多态(***)
先看一个问题
使用传统的方法来解决(private属性)
问题是:代码复用性不高,而且不利于维护
解决方案——多态
【多态基本介绍】
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承之上的
方法的多态
重写和重载就体现了多态
package Poly;
public class MethodPloy {
public static void main(String[] args) {
// 方法重载体现多态
A a = new A();
// 输入不同参数,就会调用不同的sum方法,就体现了多态
System.out.println(a.sum(1,2));
System.out.println(a.sum(1,2,3));
// 方法重写体现多态
B b = new B();
a.say(); // A say()方法 被调用
b.say(); // B say()方法 被调用
}
}
class B { // 父类
public void say(){
System.out.println("B say()方法 被调用");
}
}
class A extends B { //子类
public int sum(int n1, int n2){
return n1 + n2;
}
public int sum(int n1, int n2,int n3){
return n1 + n2 + n3;
}
@Override
public void say() {
System.out.println("A say()方法 被调用");
}
}
对象的多态
对象的多态(核心)
- 一个对象的编译类型和运行类型可以不一致
- 编译类型在定义对象时,就确定了,不能改变
- 运行类型是可以改变的
- 编译类型看定义时 = 号 的左边,运行类型看 = 号 的右边
例如:Animal animal = new Dog();【animal 的编译类型是Animal,运行类型是Dog】
animal = new Cat();【animal 的运行类型变为了Cat,编译类型还是是Animal】
【多态解决主人喂食问题】
当我们再添加新的动物类或者食物类并给它喂食,只需要新建类与创建相应对象即可!feed()方法不变!
多态的细节
向上转型(***)
多态的前提是:两个对象(类)存在继承关系
多态的向上转型:
-
本质:父类的引用指向子类的对象
-
语法:
父类类型 引用名 = new 子类类型();
-
特点:编译类型看左边,运行类型看右边
- 可以调用父类中所有的成员(需遵循访问权限规则)
- 不能调用子类中特有成员(因为在编译阶段(先编译后运行),能调用那些成员,是由编译类型来决定的!)
- 站在动物的角度,动物(猫)是不会抓老鼠的
- 最终运行效果看子类(运行类型)具体实现!即调用方法时按照从子类开始查找方法,然后进行调用,规则和前面的方法调用规则一致!
一句话:父类不能调用子类特有方法,子类重写了父类方法就先调用子类方法
package Poly.Detail; public class Animal { String name = "动物"; int age = 10; public void sleep(){ System.out.println("睡"); } public void run(){ System.out.println("跑"); } public void eat(){ System.out.println("吃"); } public void show(){ System.out.println("展示"); } } ----- package Poly.Detail; public class Cat extends Animal{ // 方法重写 public void eat() { System.out.println("猫吃鱼..."); } //Cat 特有方法 public void catchMouse(){ System.out.println("猫抓老鼠"); } } ----- package Poly.Detail; public class PolyDeatil { public static void main(String[] args) { //向上转型:父类的引用指向子类的对象 //父类类型 引用名 = new 子类类型(); Animal animal = new Cat(); //Object obj = new Cat();// 可以吗? 可以的, Object也是Cat的父类 //向上转型调用规则如下: //(1)可以调用父类中所有的成员(需遵循访问权限规则) //(2)但不能调用子类中特有成员 /* 原因: (#) 因为在编译阶段(先编译后运行),能调用那些成员,是由编译类型来决定的!而Cat是运行类型 */ // animal.catchMouse(); 编译错误 //(4)最终运行效果看子类(运行类型)具体实现!即调用方法时按照从子类开始查找方法,然后进行调用,规则和前面的方法调用规则一致! animal.eat();// 猫吃鱼... animal.run(); animal.show(); animal.sleep(); } }
向下转型
-
多态的向下转型
-
语法:(强转)
子类类型 引用名 = (子类类型)父类引用
-
只能强转父类的引用,不能强转父类的对象
-
要求父类的引用必须指向的是当前目标类型的对象
- 可以调用子类中类型中所有的成员
【参考代码同上仅多添加了一个Dog类】
-
属性重写问题
-
属性没有重写之说!属性的值看编译类型
package Poly.Detail; public class PolyDetail02 { public static void main(String[] args) { //属性没有重写之说!属性的值看编译类型 Base base = new Sub();// 向上转型 System.out.println(base.count); // 20 编译类型 Base Sub sub = new Sub(); System.out.println(sub.count);// 10 编译类型 Sub } } class Base { // 父类 int count = 20; } class Sub extends Base { // 子类 int count = 10; }
-
instanceOf比较操作符,用于判断对象的运行类型是否为XX类型或者XX类的子类型
package Poly.Detail; public class PolyDetail03 { public static void main(String[] args) { //- instanceOf比较操作符,用于判断**对象的运行类型**是否为XX类型或者XX类的子类型 BB bb = new BB(); System.out.println(bb instanceof BB); // true System.out.println(bb instanceof AA); // true //编译类型是 AA 运行类型是 BB AA aa = new BB(); System.out.println(aa instanceof AA);// true System.out.println(aa instanceof BB);// true } } class AA {} // 父类 class BB extends AA{} // 子类
【多态练习】
package Poly.Detail; public class PolyDetail02 { public static void main(String[] args) { Sub sub = new Sub(); System.out.println(sub.count);// ? sub.display();// ? Base base = sub;// 向上转型 System.out.println(base == sub);// ? System.out.println(base.count);// ? base.display();// ? } } class Base { // 父类 int count = 10; public void display(){ System.out.println(this.count); } } class Sub extends Base { // 子类 int count = 20; // 重写父类display方法 public void display(){ System.out.println(this.count); } }
输出结果:
20
20
true
10
20
动态绑定机制(***)
Java动态绑定机制:
- 当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明哪里使用
package Poly.DynamicBinding;
public class A { // 父类
public int i = 10;
public int sum() {
return getI() + 10;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
-----
package Poly.DynamicBinding;
public class B extends A{// 子类
public int i = 20;
public int sum() {
return i + 20;
}
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
-----
package Poly.DynamicBinding;
public class Test {
public static void main(String[] args) {
// a的编译类型是 A 运行类型是 B
A a = new B(); // 向上转型
System.out.println(a.sum()); // 40
System.out.println(a.sum1());// 30
}
}
当我们把子类的sum()方法和sum1()方法注释掉后结果又是怎么样的呢?——动态绑定机制
package Poly.DynamicBinding;
public class Test {
public static void main(String[] args) {
// a的 编译类型是A 运行类型是B
A a = new B(); // 向上转型
System.out.println(a.sum()); // 40 ---> 30
System.out.println(a.sum1());// 30 ---> 20
}
}
class A { // 父类
public int i = 10;
/*
子类sum方法注释掉后,将访问到父类的sum方法
getI()方法子类和父类都有,那么这里的getI()是调用父类的还是子类的呢?
*/
//动态绑定机制:当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定
/*
a的 编译类型是A 运行类型是B
因此调用的是子类B的getI()方法
a.sum() = 30
*/
public int sum() {
return getI() + 10;
}
/*
子类sum1方法注释掉后,将访问到父类的sum1方法
动态绑定机制:当调用对象属性时,没有动态绑定机制,哪里声明哪里使用
因此return i + 10; 这里的 i 为本类(父类)中的 i = 10
a.sum1() = 20
*/
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A {// 子类
public int i = 20;
// public int sum() {
// return i + 20;
// }
// public int sum1() {
// return i + 10;
// }
public int getI() {
return i;
}
}
多态的应用
-
多态数组:数组的定义类型为父类类型,里面保存的的实际元素类型为子类类型。
数组竟然还能这么用?震惊到我了!
示例应用:
多态数组的好处:遍历时编译类型是确定的, 运行类型是根据实际情况JVM来判断的!
应用升级:如何调用子类特有方法?——判断类型+向下转型
- 多态参数
方法定义的形参类型为父类类型,实参允许为子类类型(父类的引用可以是父类自己也可以指向子类——最终方法的调用(看运行类型)动态绑定机制)
应用示例1:前面案例的主人喂食问题
应用示例2:
-----父类
package PolyParameter;
public class Employee {
private String name;
double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
//计算年工资
public double getAnnual(){
return 12 * salary;
}
}
-----子类:普通员工
package PolyParameter;
public class Worker extends Employee{
public Worker(String name, double salary) {
super(name, salary);
}
//普通员工没有其它奖金,年薪直接用父类getAnnual()方法即可
@Override
public double getAnnual() {
return super.getAnnual();
}
//普通员工work()方法
public void work(){
System.out.println("员工" + getName() + "在工作");
}
}
-----子类:经理
package PolyParameter;
public class Manger extends Employee{
private double bounds;
public Manger(String name, double salary, double bounds) {
super(name, salary);
this.bounds = bounds;
}
public double getBounds() {
return bounds;
}
public void setBounds(double bounds) {
this.bounds = bounds;
}
// 经理多了奖金
@Override
public double getAnnual() {
return super.getAnnual() + bounds; // 继承的好处
}
//mange()方法
public void mange(){
System.out.println(getName() + " 是经理");
}
}
----测试类
package PolyParameter;
public class Test {
public static void main(String[] args) {
Worker tom = new Worker("tom",5000.0);
Manger jack = new Manger("jack",8000.0,30000);
Test test = new Test();
//1.计算年终奖
// 传子类对象
test.showEmpAnnual(tom);// 60000.0
test.showEmpAnnual(jack);// 126000.0
//2.调用子类特有方法
test.testWork(tom);// 员工tom在工作
test.testWork(jack);// jack 是经理
}
// 参数多态(父类的引用可以指向子类)
//showEmpAnnual(Employee e)
//实现获取任何员工对象的工整,并在main()方法中调用该方法[e.getAnnual()]
public void showEmpAnnual(Employee e){
System.out.println(e.getAnnual()); // 动态绑定机制
}
//testWork()方法:如果是普通员工则调用work()方法,如果是经理则调用mange()方法
// work()和mange()是子类特有的方法!(父类无):Employee e 编译类型是Employee,运行类型是e,具体根据JVM来判断(传进来的对象)
//Employee e 其实就是一个上转型
//解决:调用子类特有方法————判断类型 + 向下转型
public void testWork(Employee e){
if(e instanceof Worker){ // 判断运行类型
((Worker) e).work(); //向下转型,调用子类特有方法
}else if(e instanceof Manger){
((Manger) e).mange();// 向下转型操作
}else{
System.out.println("不做处理...");
}
}
}
Object类详解
equals方法
==
与equals
的对比【面试题】+ jdk查看原码
==是一个比较运算符
-
==: 既可以判断基本类型,又可以判断引用类型
-
==: 如果判断的是基本类型,判断的是值是否相等。
//==: 如果判断的是基本类型,判断的是 值 是否相等 int x1 = 10; int x2 = 10; double x3 = 10.0; System.out.println(x1 == x2);//true System.out.println(x1 == x3);//true
-
==: 如果判断的是引用类型,判断的是地址是否相等,即判断是不是同一个对象
package Equals; public class Test01 { public static void main(String[] args) { //==: 如果判断的是引用类型,判断的是地址是否相等,即判断是不是同一个对象 A a = new A(); A b = a; A c = b; System.out.println(a==c);// ? true System.out.println(b==c);// true B obj = a; System.out.println(obj==c);// true } } class B{} class A extends B{}
- equals方法是Object类中的方法,只能判断引用类型。
idea查看Jdk原码:鼠标光标放在要查看的方法上,直接输入
ctrl + b
查看某个类所有方法:
ctrl + F12
- 默认判断的是地址是否相等,子类(Object类是所有类的父类)往往重写该方法,用于判断内容是否相等。
/*
Object类 equals()方法原码
//默认判断地址是否一样
public boolean equals(Object obj) {
return (this == obj);
}
子类往往重写该方法,用于判断内容是否相等 String类中的equals()方法原码(重写了父类equals()方法)
public boolean equals(Object anObject) {
if (this == anObject) { // 如果是同一个对象(地址相同)
return true; // 返回true
}
if (anObject instanceof String) { // 判断类型
String anotherString = (String)anObject; // 向下转型
int n = value.length;
if (n == anotherString.value.length) { // 如果长度相同
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) { // 比较每一个字符
if (v1[i] != v2[i])
return false;
i++;
}
return true; // 如果两个字符串每一个字符都相同,则返回true
}
}
return false;
}
*/
在看个例子
【小练习】
写出输出结果:
package Equals;
public class EqualsTest01 {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "tom";
Person p2 = new Person();
p2.name = "tom";
System.out.println(p1 == p2);// 引用类型——判断是否为同一个对象(地址)
System.out.println(p1.name.equals(p2.name));// p.name是String类型,重写了equals()方法——判断内容是否一样
System.out.println(p1.equals(p2));//p1,p2属于Person类,该类并没有重写equals()方法(继承父类equals()方法,即判断地址)
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1.equals(s2));
System.out.println(s1 == s2);
}
}
class Person{
public String name;
}
输出结果:
false
true
false
true
false
hashCode方法
小结:(可以当作地址来看但它本质上不是地址)
- 提高具有哈希结构的容器的效率
- 两个引用,如果指向的是同一个对象,则哈希值肯定一样
- 两个引用,如果指向的是不同对象,则哈希值是不一样的
- 哈希值主要根据地址号来!不能将哈希值完全等价于地址
- 在后面的集合中hashCode如果需要的话,也会重写
package hashCode;
public class HashCode {
public static void main(String[] args) {
AA aa = new AA();
AA aa2 = new AA();
AA aa3 = aa;
System.out.println("aa.hashCode()="+ aa.hashCode());
System.out.println("aa2.hashCode()="+ aa2.hashCode());
System.out.println("aa3.hashCode()="+ aa3.hashCode());
}
}
class AA{}
aa.hashCode()=460141958
aa2.hashCode()=1163157884
aa3.hashCode()=460141958
toString方法
基本介绍:
默认返回:全类名 + @ + 哈希值的十六进制
/*
Object toString()原码
//(1)getClass().getName() 类的全类名(包名+类名)
//(2)Integer.toHexString(hashCode()) 将hashCode的值转成16进制字符串
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
*/
子类往往重写toString方法,用于返回对象的属性信息(快捷键:alt + insert
),当然我们也可以自己定制。
当我们输出一个对象时,toString()方法会被默认调用
finzlize方法
finzlize方法:当垃圾收集确定不再有对该对象的引用时,垃圾收集器在对象上调用该对象。
- 当对象被回收时,系统自动调用该对象的finzlize方法。子类可以重写该方法,做一些释放资源的操作
- 什么时候被回收:当某个对象没有任何引用时,则jvm就认为这个对象是一个垃圾对象,就会时候垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finzlize方法。
- 垃圾回收机制的调用,是由系统来决定(即有自己的GC算法),也可以通过System.gc()主动触发垃圾回收机制。
注:在实际开发中,几乎不会用finzlize方法,更多的是为了应付面试
断点调试(debug)
- 在开发中查找错误时,可以使用断点调试,一步一步的看源码执行过程,从而发现错误所在。
- 在断点调试过程中,是运行状态,是以对象的运行类型来执行的。
【基本介绍】
- 断点调试是指在程序的某一行设置一个断点,调试时,程序运行到这一行就会停住,然后你可以一步一步的往下调试,调试过程可以看各个变量当前的值,出错的话,调试到出错的代码行即显示错误,停下。进行分析从而找到这个bug。
- 断点调试也能帮助我们查看Java底层源码的执行过程。
【断点调试快捷键】
F7:跳入方法内
F8:逐行执行代码
F9:resume,执行下一个断点
shift+F8:跳出方法
断点调试案例
案例01、看一下变量的变化情况
)
案例02、看一下数组越界的异常
案例03、如何追源码,看看Java设计者是怎么实现的(提高编程思想)
Debug如何查看源码:
- 方法1:
Alt + shift + F7
强行步入 - 方法二:
F7:(step into)
(前提是需要设置好:setting-->Debugger-->Stepping,将下图红框框部分改为不选中即可)
追踪Arrays.sort()方法底层实现:
shift+F8:跳出方法
案例04、如何执行到下一个断点F9;resume
断点在Debug过程中,动态的下断点,可以帮助我们查看业务逻辑能不能进入某个断点,有助于我们观看复杂的算法、架构等等。
断点调试练习:
-
使用断点调试的方法,追踪一下对象创建的过程。
F7:(step into)或者
Alt + shift + F7
强行步入
- 使用断点调试,查看动态绑定机制是如何工作的
分别将B类和A类中的sum()方法注释,然后动点调试!
B类sum()方法注释时执行过程:
package Poly.DynamicBinding;
public class Test {
public static void main(String[] args) {
// a的 编译类型是A 运行类型是B
A a = new B(); // 向上转型
System.out.println(a.sum()); // 40 ---> 30
System.out.println(a.sum1());// 30 ---> 20
}
}
class A { // 父类
public int i = 10;
/*
子类sum方法注释掉后,将访问到父类的sum方法
getI()方法子类和父类都有,那么这里的getI()是调用父类的还是子类的呢?
*/
//动态绑定机制:当调用对象方法时,该方法会和该对象的内存地址/运行类型绑定
/*
a的 编译类型是A 运行类型是B
因此调用的是子类B的getI()方法
a.sum() = 30
*/
public int sum() {
return getI() + 10;
}
/*
子类sum1方法注释掉后,将访问到父类的sum1方法
动态绑定机制:当调用对象属性时,没有动态绑定机制,哪里声明哪里使用
因此return i + 10; 这里的 i 为本类(父类)中的 i = 10
a.sum1() = 20
*/
public int sum1() {
return i + 10;
}
public int getI() {
return i;
}
}
class B extends A {// 子类
public int i = 20;
// public int sum() {
// return i + 20;
// }
// public int sum1() {
// return i + 10;
// }
public int getI() {
return i;
}
}
学习内容源自视频:b站韩顺平老师说Java