06-Java基础-面向对象概述

面向对象概述

概述

编程语言是人类创造的,因此里面很多思想来源于生活。程序员使用编程技术开发软件也是为了解决生活中一些繁琐的问题,编程无非就是将生活中的一些问题利用代码交给计算机解决。这一节将要学习一种编程思想-面向对象,Java就是一门面向对象设计的语言。

面向过程和面向对象的区别

通过对比,来更深入的了解下面向对象

  • 面向过程(c语言)主要是把问题分解成多个不同的步骤,然后把各个步骤变成方法,它更强调过程。
  • 面向对象(java语言)会把问题分解成各个对象,然后各个对象之间进行交互,每个对象内部封装了属性和方法。

举例说明,比如吃烧烤

面向过程:

  1. 去买羊肉、羊腰、签子、木炭、烤炉、调味料
  2. 腌制羊肉、穿羊肉
  3. 将木炭放到烤炉里面并引燃
  4. 烧烤
  5. 开吃
  6. 收拾

需要一步一步去做

面向对象:

  1. 去烧烤店
  2. 跟服务员点菜,20串羊肉、10个大腰子
  3. 开吃
  4. 跟收银员结账

去烧烤店里面,找服务员点菜,烧烤的事交给别的对象去做

面向过程注重过程,面向对象注重对象之间的交互。面向对象将复杂的事情简单化,我们从行动者变成了指挥者。面向对象开发就是去创建对象,指挥对象做事情。我们在下一篇解释什么是对象。

网上还有另一个比较受欢迎的解释方式就是:

面向过程是编年体,面向对象是纪传体

在编年体里面,会将100年之间发生的事情一件一件记录下来,我们看完之后对事情发生的过程比较清晰,但是每个人物都做了什么事,就记不清了。

在纪传体中,会围绕着一些事情的主角来记录,所以跟这件事相关的内容都写到一起,这样我们就可以很方便的查到关于某件事都有哪些人参加,分别做了什么。另外对于某件事情,如果有新的人物加入,那么也会比较容易记录。试想在编年体中加入新的人物就不太方便了,需要一点一点的对日期。从代码的角度上来说,面向对象的扩展性更强,说的好理解一些就是利于将来代码的修改。

image-20230706143940886

面向对象三大特征

  • 封装(encapsulation)
  • 继承(inherit)
  • 多态(polymorphism)

先记住这三个特征,后面的章节里面会详细进行介绍

类与对象

属性和行为

我们学习编程最主要的一个目的就是解决日常生活中的问题,将繁琐的事物交给计算机去处理。这就会需要程序员将日常生活中的事物进行抽象,然后在使用编程语言编写出来。
如何将日常生活中的事物进行抽象呢?一般事物都具有下面两个特点:

  • 属性-描述该事物的信息,一般是事物上面的名词
  • 行为-描述该事物能做什么,一般是动词

比如学生的属性:

学号:1001
姓名:张三
性别:男
年龄:20

学生的行为:

学习:好好学习,天天向上
谈恋爱:找个会写代码的妹子
做运动:打篮球

在一个Java程序里面,是由多个class)构成的,类是由属性和方法组成的,即类=属性+方法

这里的说的属性就是变量,在java中变量根据其所在的位置分为下面两种:

成员变量:在类的内部,方法的外部定义的变量叫做成员变量。上面提到的属性特指成员变量。

局部变量:在方法内部定义的变量叫做局部变量。之前我们使用的都是局部变量。

关于成员变量和局部变量之间的差异,会在后面的章节进行介绍。

我们可以将现实生活中的事物编写成一个类,通过操作这个类来解决问题。

如何定义类?

语法如下:

类的修饰符 class 类名 extends 父对象名称 implements 接口名称 {
   类体,成员变量和方法组成
}

extends和implements还没有学过,目前我们定义类的格式是这样的:

类的修饰符 class 类名{
   类体,成员变量和方法组成
}

这里根据上面分析的学生来定义一个类

public class Student{
    //成员变量可以不初始化赋值
    
    //学号
    int id; 
    
    //姓名
    String name;
    
    //性别
    boolean sex;
    
    //年龄
    int age;
    
    //这里面的方法可以不加static修饰,关于static后面会详细讲解
    public void study() {				//定义学习的方法
        System.out.println("学生学习");
    }

    public void love(String name) {				//定义谈恋爱的方法
        System.out.println("我在跟" + name + "谈恋爱");
    }
    
    //做运动
    public void takeExercises(String sport){
        System.out.println(sport);
    }
}

学生类定义好了,那如何表示上面的张三这个人呢?这时需要使用对象

类和对象

类是一组相关的属性和行为的集合,类一般都是泛指某一种事物,对象就是该事物的具体体现。类是对象的抽象,对象是类的具体,如果理解了这句话,那对于类和对象之间的关系就理清了。
比如:

类-学生
对象-张三

当然,对象可以是李四,王五,赵六,只要是学生就行。
再比如:

类-歌手
对象-周杰伦,刘德华

类-运动员
对象-姚明,刘翔

对象的创建和使用

通过创建对象的方式,将张三表示出来,使用new关键字来创建对象

public class StudentTest01{

    public static void main(String[] args){

        //创建对象的格式:类名 对象名 = new 类名();
        //对象名:是合法的标识符即可,建议使用驼峰命名法
        Student s = new Student();
        //Student s1 = new Student();一个类可以创建多个对象,李四、王五、赵六等
        //通过对象名.变量名的方式使用成员变量
        s.name = "张三";//给学生名字赋值
        s.age = 20;
        s.sex = true;
        s.id = 1001;

        System.out.println(s.id);
        System.out.println(s.name);
        System.out.println(s.age);
        System.out.println(s.sex ? "男":"女");

        //通过对象名.方法名(...)的方式调用方法
        s.study();
        s.love("赵六");
        s.takeExercises("打篮球");	
    }
}

成员变量和局部变量

成员变量和局部变量的区别

之前简单的介绍过这两种变量,这一节里面,我们来详细的分析一下两者的区别
成员变量:写在类体的里面,方法体的外面,声明时可以不进行初始化值,可以被本类或其他类的方法进行调用。
局部变量:写在方法体的里面,声明时必须进行初始化,只能在声明局部变量的方法内进行调用。

在一个类中,成员变量和局部变量是可以同名的,比如有同名成员变量和局部变量都叫做i,在局部变量所在的方法中打印i时会优先使用局部变量,即就近原则。下面代码中的m1方法中定义了跟成员变量同名的局部变量i,在打印语句中使用了i,此时系统会先从m1方法中的局部变量开始找,如果找到i了就直接打印,如果没有找到,则从成员变量中查找。

两者在代码中的体现:

/**
 * 该案例对局部变量和成员变量进行比较
 */
public class TestVariable {
    //成员变量  在整个类中有效    成员变量有默认值
    int i;

    public void m1(){
        //局部变量   如果成员变量和局部变量出现了命名冲突,在使用的时候局部优先
        //局部变量在使用的时候  必须先声明,后赋值,再使用
        int i = 30;
        System.out.println(i);
    }
    
    public void m2(){
        /*
        	这里会从当前方法里面查找i,没有找到,就会从类体里面查找
        	所以会使用成员变量i
        */
        System.out.println(i);
    }

    public static void main(String[] args) {
        TestVariable tv = new TestVariable();
        System.out.println(tv.i);
        tv.m1();
    }
}

数据类型的默认值

如果只声明成员变量不对其进行赋值,那么这些成员变量的默认值都是什么呢?
请看如下代码:

class Variable {
    
    byte b;
    short s;
    char c;
    int i;
    long l;
    float f;
    double d;
    boolean boo;
    String str;

    public static void main(String[] args) {
        Variable var = new Variable();
        System.out.println(var.b);//0
        System.out.println(var.s);//0
        System.out.println(var.i);//0
        System.out.println(var.l);//0
        System.out.println(var.f);//0.0
        System.out.println(var.d);//0.0
        System.out.println(var.boo);//false
        System.out.println(var.str);//null
        System.out.print(var.c);//char的默认值是\u0000
    }
}

基本数据类型的默认值

byte,short,int,long   整数型默认值都是0
float,double  浮点型默认值是0.0
boolean false
char 	\u0000 

引用数据类型成员变量的默认值:null

以上的默认值需要牢牢记住。

对象内存图解

创建一个对象

通过画图来看下对象在内存中是如何分配的,这样更有助于初学者理解对象。
学生类

class Student{
        
    //学号
    int id; 
    
    //姓名
    String name;
    
    //性别
    boolean sex;
    
    //年龄
    int age;
    
}

学生测试类:

public class StudentTest01{
    
    public static void main(String[] args){
        Student s = new Student();
        s.name = "张三";
        s.age = 20;
        s.sex = true;
        s.id = "1001";
    }
}

在JVM内存里面主要有以下2个区域(除此之外还有一些其他的区域,目前先不讨论):

  • java虚拟机栈(stack):被调用的方法会压栈,这里还会存放方法中的局部变量
  • java堆(heap):创建出来的对象会存放在堆中,成员变量也存储在这里

代码内存图:

image-20230706144148961

通过上图来看,程序是这样执行的:

  1. main方法压栈,开始执行里面的代码
  2. Student s = new Student()会在堆里面开辟一块空间来存储创建的Student对象,假设内存地址是0x0101,那么s就会指向这个内存地址,s存储在栈内存中,这样通过栈内存中的s就可以找到堆内存中的对象了。
  3. 通过s.的方式访问堆中的对象,并为其成员变量进行赋值
  4. 方法弹栈
  5. 程序执行结束

这里面主要说明的一点就是图中的实线,这个实线表示的是s指向Student对象堆内存的地址,s本身并不是对象,而是一个引用指向。比如酒店里面的房间号1024,1024本身不是房间,它是指向了这个房间的号码,方便住宿人员快速找到该房间。

创建两个对象

public class StudentTest01{

    public static void main(String[] args){

        Student s1 = new Student();
        s1.name = "张三";
        s1.age = 20;
        s1.sex = true;
        s1.id = "1001";

        Student s2 = new Student();
        s2.id=1002;
        s2.name="李四";
        s2.age=21;
        s2.sex=false;
    }
}

在执行 Student s2 = new Student()时会再次在堆中开辟一块新的内存空间来存放新创建出来的Student对象。

image-20230706144215376

三个指向两个对象

public class StudentTest01{
    
    public static void main(String[] args){
        
    Student s1 = new Student();
    s1.name = "张三";
    s1.age = 20;
    s1.sex = true;
    s1.id = "1001";
        
    Student s2 = new Student();
    s2.id=1002;
    s2.name="李四";
    s2.age=21;
    s2.sex=false;
    
    Student s3 = s2;
        
    }
}

上面代码将s2指向的地址赋给s3,相当于s2和s3指向同一个内存地址,这里new了两次所以一共有两个对象,但是在栈空间里面有s1,s2,s3三个引用(也叫变量,由于3者都是引用数据类型,一般会被成为引用)。

image-20230706144241686

取消指向

public class StudentTest01{

    public static void main(String[] args){

        Student s = new Student();
        s.name = "张三";
        s.age = 20;
        s.sex = true;
        s.id = "1001";

        s = null;//将s设置为null
        //s.study();报出NullPointerException(空指针异常)
    }
}

上面代码将s赋值为null,这样s就不会指向0x0101这个内存地址,我们也就不能再通过s访问堆中的对象了。如果堆里面的对象没有被指向,Java里面有个垃圾回收器会将这些对象进行回收来腾出内存空间给其他需要使用的对象。开发时不需要手动设置为null,垃圾回收器会通过算法自动回收。

image-20230706144305650

封装性和private关键字

封装

这一节来看下面向对象三大特征之一的封装。
什么是封装?
封装是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。一个类中的内容不要全部暴露给外部,仅将部分内容暴露出来,这就是封装。
封装的优点:
隐藏代码的实现细节,提高安全性。倘若一个类中的内容全部暴露出来,别的地方可以随意操作,这就有可能出现安全问题。
举个例子,我们日常生活中的插座,如果没有封装外壳直接使用零线火线的话会比较危险,加上外壳封装之后使用起来既方便又安全。

private关键字

使用private关键字可以将类中的属性或方法封装起来。private修饰的属性或者方法只能在当前类中使用

来看看下面代码中所描述的问题。

public class Person {
    String name;//姓名
    int age;//年龄

}

上面定义了一个Person类,下面写一个PersonTest01类对其进行测试

public class PersonTest01{
    
    public static void main(String[] args){
        
        //1.创建Person类型的对象
        Person p = new Person();
        System.out.println(p.age); //0
        
        p.age = -10;//这个地方的数据不合理
        
    }
}

上面的PersonTest01中人的年龄出现了-10,这显然是一个不合法的数据,在Person类的外部可以直接操作其内部的成员变量,这样是不安全的,所以应该将Person类中的age封装起来,不能让外界直接访问,这里我们可以使用private修饰符将age进行封装。

将成员变量封装之后又遇到了一个新的问题,Person类的外部不能操作这个成员变量了,因此需要在Person中为每个封装的成员变量添加对应的get、set方法。

set方法:外界可以通过该方法给成员变量赋值,比如要给成员变量name赋值,那么方法名会叫setName。

get方法:外界可以通过该方法获取成员变量的值,比如要获取成员变量name的值,那么方法名会叫getName。

具体请参考下面代码示例:

public class Person {
    private String name;
    private int age;
    
    /*
        添加private修饰之后,外界无法直接访问age了。
        这时需要提供两个方法供外界访问即set方法和get方法。
        目前这里先使用_的方式来命名变量,这样做的目的是区分成员变量和局部变量
        后面学了this就可以不用下划线了
    */

    //对外提供两个公开的方法。
    //set  赋值
    public void setAge(int _age){
        //安全控制,判断年龄是否合法
        if(_age<0 || _age>120){
            System.out.println("年龄不合法");
            return;
        }
        //将方法参数传入的_age的值赋值给成员变量age
        age = _age;
    }
    
    //外界可以通过get方法获取成员变量age的值
    public int getAge(){ 						
        return age;
    }

    public void setName(String _name){
        name = _name;
    }

    public String getName(){
        return name;
    }
}

创建测试类,创建好Person对象之后,调用相应的set、get方法赋值和取值

public class PersonTest02{

    public static void main(String[] args){

        //创建Person类型的对象
        Person p = new Person();
        
        //不能再这样操作Person中的成员变量了
        //p.name,p.age,编译会报错
        
        //利用set方法给name和age赋值
        p.setName("郝仁");
        p.setAge(20);
        //利用get方法取值
        System.out.println(p.getName());
        System.out.println(p.getAge());
    }
}

private使用总结:

  • 将成员变量用private修饰
  • 提供对应的getXxx()和setXxx()方法
  • private仅仅是封装的一种体现形式,封装不是私有

private修饰的方法只能在当前类中使用

private修饰方法的效果跟修饰属性类似,都是只能在当前类中使用。

public class Student{
    private void study(){
        System.out.println("好好学习");
    }
    
    public void love(){
        study();//因为love和study两个方法在同一个类中,所以这里可以调用study方法
    }
}

创建测试类

public class StudentTest{
    public static void main(String args){
        Student s = new Student();
        s.study();//这里会报错,study方法是private修饰的,因此不能直接调用
    }
}

方法调用时参数传递的问题

声明方法时,形参除了可以使用基本数据类型,还可以使用引用数据类型,这两种参数在方法调用传递的时候会有一些差异,在使用的时候需要注意

基本数据类型的参数传递

先来看看这段代码,m1方法中的i和main方法里面的i的值分别是多少?

public class ParamTest01{

    public static void m1(int i){
        i++;
        System.out.println("m1里面的变量i=" + i); 
    }
        
    public static void main(String[] args){
        
        //局部变量
        int i = 10;
        
        //调用
        m1(i);
        
        System.out.println("main里面的变量i=" + i);
    }

}

打印的结果是:
m1里面的变量i=11
main里面的变量i=10
出现这个结果的原因就是main方法里面的i和m1方法里面的i分别占用的是两块不同的内存,请看下图,main方法栈帧和m1方法栈帧分别有两个i,所以这两个i是不同的。main方法中调用m1方法是仅仅是将i的值10传了过去。

image-20230706144422972

引用数据类型的参数传递

在Java语言里面除了基本数据类型,其他的都是引用数据类型。当方法传递的参数是引用数据类型时会是什么样子呢?请看下面代码

定义一个Animal类

class Animal{
    
    private int age;
    
    public void setAge(int _age){
        age = _age;
    }
    
    public int getAge(){
        return age;
    }
    
}

定义一个Animal测试类,请问下面程序打印的结果是什么?

public class AnimalTest01{
    
    //方法传递的参数是引用数据类型Animal
    public static void m1(Animal a){
        a.setAge(66);
        System.out.println("m1中的age=" + a.getAge()); 
    }
    
    public static void main(String[] args){
        
        //创建Animal对象
        Animal a = new Animal();
        a.setAge(10);
        m1(a);
        
        System.out.println("main中的age" + a.getAge()); 
        
    }

}

上面打印的结果:
m1中的age=66
main中的age=66
两个打印的结果都是66,与基本数据类型的过程是不一样的,这是因为方法参数传过去的是对象的内存地址,m1方法里面的a和main方法里面的a所指向的是同一个Animal对象,所以当m1方法中修改了Animal对象中的age之后,main方法里面在获取age时,值也发生了改变。

image-20230706144441668

构造方法

构造方法的概念

构造方法(constructor),有的地方叫做构造器或者构造函数。在创建对象的时new后面的部分就是调用构造方法,比如下面的Student();这个就是调用了Student的构造方法。

Student s = new Student();

在创建对象时会调用构造方法,因此你在创建对象时想要做的事情就可以写到构造方法中。

构造方法格式特点

  • 方法名与类名相同(注意大小写也要与类名一致)
  • 没有返回值类型
  • 没有void修饰
  • 方法体中没有return;
  • 如果一个类没有提供任何构造方法,系统默认提供无参数构造方法。
  • 如果一个类已经手动的提供了构造方法,那么系统不会再提供任何构造方法。
  • 构造方法支持重载。

无参构造方法的使用

请看下面示例,定义一个User类:

public class User{
    
    //定义无参构造方法
    User(){
        System.out.println("User的无参数构造方法执行!");
    }

}

创建一个UserTest01测试类:

public class UserTest01{

    public static void main(String[] args){
    
        User u = new User();
        //u.User();不能手动调用构造方法
    }

}

上面代码在创建User对象的时候,就打印出了构造方法里面的语句,说明在创建对象的时候会默认执行无参构造方法。构造方法不能手动调用。

有参构造方法的使用

构造方法是可以构成重载的,可以写个有参数的构造方法为对象进行数据初始化

public class User{

    private String name;
    private int age;
    
    //定义无参构造方法
    User(){
        System.out.println("User的无参数构造方法执行!");
    }
    //定义有参构造方法,在创建对象的时候为成员变量赋值
    User(String _name,int _age){
        name = _name;
        age = _age;
    }

    public String getName(){
        return name;
    }

    public int getAge(){
        return age;
    }

}

在测试类里面进行赋值

public class UserTest01{

    public static void main(String[] args){
        //调用了有参构造方法
        User u = new User("化腾",40);
        System.out.println(u.getName());
        System.out.println(u.getAge());
        
    }

}

上面的代码演示了如何使用构造方法给对象数据进行初始化,通过调用有参的构造方法将数据传入,构造方法内部再将这些数据赋值给成员变量,这样在创建好对象之后里面的属性就有值了。

接下来再看一个问题,将上面User方法里面的无参构造方法去掉

public class User{

    private String name;
    private int age;
    
    //定义无参构造方法
    /*去掉无参的构造方法
    User(){
        System.out.println("User的无参数构造方法执行!");
    }
    */
    //定义有参构造方法
    User(String _name,int _age){
        name = _name;
        age = _age;
    }

    public String getName(){
        return name;
    }

    public int getAge(){
        return age;
    }


}

在测试类里面创建User对象

public class UserTest01{

    public static void main(String[] args){
    
        User u = new User();//报错
        
    }

}

在编译时上面代码将会报错,这是因为我们已经手动的编写了一个带有参数的构造方法,那么系统将不会为我们提供默认的无参构造方法了。上面代码在创建对象时会调用无参的构造方法,此时在User类中没有无参构造方法,所以报错。
**注意:**如果我们还想使用无参构造方法,就必须自己写出。建议在提供有参构造方法之后自己再提供无参构造方法

this关键字

this是什么?

this是java里面的一个关键字,是一种引用类型,在堆(heap)中的每个java对象上都有一个this指向自己。this代表着当前对象的引用。比如创建了两个对象A和B,那么就会有两个this分别指向A和B,通常会使用this来区分同名的成员变量和局部变量。

可以将this想象成为酒店房间的门牌号,酒店每一个房间就是一个对象,每个房间都有一个门牌号指向了这个房间对象。

image-20230706144624900

this的使用

1.区分成员变量和局部变量。在类中编写代码的时候实际上还没有对象,this其实就是表示将来创建的对象,成员变量与局部变量同名的时候,通过this.成员变量名的方式就可以操作成员变量了。
例:

/*
    创建一个歌手类
*/
class Singer{

    private String name;
        
    /*
    以前在编写set方法时为了将传入参数的名字和成员变量的名字做区分
    这两个变量的名字是不一样的
    public void setName(String _name){
        name = _name;
    }
    */

    /*
        因为this代表当前对象的引用,所以可以使用this.变量名的方式调用成员变量
    */
    public void setName(String name){
        //this.name表示的是成员变量name,等号后面的name表示方法参数传入的参数
        this.name = name;
    }
    
    public String getName(){
        //return name;
        return this.name;
    }

    //还可以使用this.方法名的方式调用当前对象的方法
    //下面两种方式都可以调用到m2方法
    public void m1(){
        this.m2();
        m2();
    }
    
    public void m2(){
        System.out.println("TESTING");
    }
}

2.this可以调用构造方法

语法:this(实参);
注意:使用this调用构造方法时,this必须出现在构造方法的第一行

例:

public class Student {
    
    private String name;
    
    public Student() {
        //调用有参构造方法
        this("jack");
    }

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

    public String getName() {
        return name;
    }
}

创建测试类:

public class StudentTest {
    public static void main(String[] args) {
        //调用无参构造
        Student s = new Student();
        
        System.out.println(s.getName());
    }
}

调用构造方法的使用场景

编写一个日期类,在创建对象的时候可以指定年月日,如果是调用其无参构造创建对象的话,需要设置默认日期为1970年1月1日。

/*
    定义一个日期类
*/
class MyDate{
    
    //年
    private int year;
    //月
    private int month;
    //日
    private int day;
    
    //无参构造方法
    MyDate(){
        //通过this调用有参的构造方法
        this(1970,1,1);//必须出现在第一行,否则将编译报错
        //构造方法不能这样调用
        //MyDate(1970,1,1);Error
    }
    
    //构造方法
    MyDate(int year,int month,int day){
        this.year = year;
        this.month = month;
        this.day = day;
    }
    
    //set和get方法
    public int getYear() {
        return year;
    }
    public void setYear(int year) {
        this.year = year;
    }
    public int getMonth() {
        return month;
    }
    public void setMonth(int month) {
        this.month = month;
    }
    public int getDay() {
        return day;
    }
    public void setDay(int day) {
        this.day = day;
    }
}

public class ThisTest01{
    
    public static void main(String[] args){
        //调用无参构造
        MyDate md = new MyDate();
        
        System.out.println(md.getYear() + "年" + md.getMonth() + "月" + md.getDay() + "日");
        
    }
    
}			

再谈局部变量和成员变量

先看下面代码:

public class ActorTest01 {
    
    public static void main(String[] args){
        
        Actor a = new Actor();
        a.setName("范冰冰");
        a.act1();
        a.act2();
    }

}

class Actor{
    
    private String name;
    
    public void act1(){
        //定义一个局部变量name
        String name = "周润发";
        System.out.println("name=" + name);//这里打印出来的是局部变量name的值
        System.out.println("this.name=" + this.name);
    }
    
    public void act2(){
        System.out.println("name=" + name);//这里打印的是成员变量name的值
    }
    
    public String getName() {
        return name;
    }

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

在Java里面,局部变量和成员变量的变量名是可以相同的,如果有相同的变量名,则可以通过this调用成员变量

练习

1.写一个长方形类,里面提供计算周长和面积的方法

2.怪兽初始位置(11,11),玩家初始位置(0,0),怪兽每次移动(1,1)即怪兽移动1次,其位置变为(12,12),再移动一次,其位置变为(13,13),怪兽移动1次,玩家会移动2次,即(2,2),编写程序计算玩家和怪兽是否能相遇,若能相遇,请问玩家移动了几次?

答案

练习1:

class RectangleTest {							
    public static void main(String[] args) {
        Rectangle r = new Rectangle(10,20);
        System.out.println(r.getLength());		
        System.out.println(r.getArea());		
    }
}
/*

    成员变量:
        宽width,高height
    成员方法:
        setXxx和getXxx
        求周长:getLength()
        求面积:getArea()
*/
class Rectangle {
    
    //宽
    private int width;
    //高				
    private int height;				

    public Rectangle(){}			//空参构造

    public Rectangle(int width,int height) {
        this.width = width;			//有参构造
        this.height = height;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getWidth() {			
        return width;
    }

    public void setHeight(int height) {	
        this.height = height;
    }

    public int getHeight() {			
        return height;
    }
    
    //计算周长
    public int getLength() {		
        return 2 * (width + height);
    }
    
    //计算面积
    public int getArea() {			
        return width * height;
    }
}

练习2:

创建位置类Position,里面记录位置信息

/*
    位置
 */
public class Position {

    private int x;

    private int y;

    public Position() {
    }

    public Position(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

}

创建怪兽类,里面包含位置信息:

/*
    怪兽
 */
public class Monster {

    //名字
    private String name;

    //位置
    private Position position;

    public Monster(String name, Position position) {
        this.name = name;
        this.position = position;
    }

    public void move(int x, int y) {
        position.setX(position.getX() + x);
        position.setY(position.getY() + y);
    }

    public String getName() {
        return name;
    }

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

    public Position getPosition() {
        return position;
    }

    public void setPosition(Position position) {
        this.position = position;
    }
}

创建玩家类,里面包含位置信息

/*
    玩家
 */
public class Player {

    //名字
    private String name;

    //位置
    private Position position;

    public void move(int x,int y) {
        position.setX(position.getX() + x);
        position.setY(position.getY() + y);
    }

    public Player(String name, Position position) {
        this.name = name;
        this.position = position;
    }

    public String getName() {
        return name;
    }

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

    public Position getPosition() {
        return position;
    }

    public void setPosition(Position position) {
        this.position = position;
    }
}

创建测试类

public class GameTest {
    public static void main(String[] args) {
        //创建怪兽对象和其位置信息对象
        Position p1 = new Position(11, 11);
        Monster monster = new Monster("熔岩巨兽", p1);

        //创建玩家对象和其位置信息对象
        Position p2 = new Position(0, 0);
        Player player = new Player("剑圣", p2);

        //定义布尔变量判断是否会相遇
        boolean meet = false;
        //记录移动的次数
        int count = 0;

        //编写死循环让两者不断移动判断是否能相遇
        while (true) {
            //判断位置信息是否相等,若相等,则说明相遇
            if (monster.getPosition().getX() == player.getPosition().getX() && monster.getPosition().getY() == player.getPosition().getY()) {
                meet = true;
                break;
            } else if (monster.getPosition().getX() < player.getPosition().getX() || monster.getPosition().getY() < player.getPosition().getY()) {
                //若玩家的位置信息大于怪兽的位置信息,则永远不会相遇
                break;
            } else{
                //怪兽移动1次
                monster.move(1, 1);
                //玩家移动2次
                player.move(2, 2);
                //记录玩家移动的次数
                count += 2;
            }

        }

        //判断是否相遇
        if (meet) {
            System.out.println("玩家移动了" + count + "次,与怪兽相遇了");
        }else {
            System.out.println("不会相遇");
        }
    }
}

static关键字

什么是static?

在java中static是一个关键字表示静态的,它可以修饰以下内容:

  • 修饰变量,被static修饰的变量叫做静态变量,静态变量在类初始化阶段赋值,并且只赋值一次,当某个属性可以被一个类的多个对象共享时,可以将该变量设置为静态变量,比如圆周率PI,无论半径是多大的圆形对象,他们的圆周率都是一样的。静态变量的使用请看下面例1
  • 修饰方法,被static修饰的方法叫做静态方法,不用创建对象就能直接访问该方法,即使用类名.静态方法名的方式。main方法就是一个静态方法,后面在接触一些工具方法的时候往往会将其声明为静态方法。静态方法的使用请看下面例2
  • static可以定义静态语句块,静态语句块在类初始化阶段执行,只执行一次,按照自上而下的顺序执行,且在构造方法之前执行。程序执行时jvm会先对类进行初始化操作,这个操作完成后才能创建其对象。静态语句块的使用请看例3

static修饰的变量、方法、代码块都是隶属于**类(class)**级别的,跟对象无关。某一类物体如果可以被多个其他物体所共享,那么可以将这类物体使用static修饰。

再比如黄金的颜色,不管是金戒指还是金项链,颜色都是一样的,因此黄金的颜色可以用static修饰。

例1:

public class StaticTest01 {

    public static void main(String[] args){

        Employee e = new Employee();
        //可以通过对象名.静态方法名的方式访问,工作中不这样使用
        System.out.println(e.company);
        //可以通过类名.静态方法名的方式访问,工作中使用这种写法
        System.out.println(Employee.company);
        e = null;
        System.out.println(e.company);//不会报空指针,说明静态变量跟对象无关。
    }

}

//阿里巴巴员工类,在阿里工作的员工其公司属性值都是阿里巴巴,所以公司属性前加了static
class Employee{

    private String name;

    static String company = "阿里巴巴";

    public String getName() {
        return name;
    }

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

例2,静态方法是属于类级别,跟对象无关,所以其内部不能使用this关键字

class StaticTest02{

    //实例变量
    int i;

    //实例方法
    //实例方法必须使用“引用.”调用
    public void m1(){
        System.out.println("m1方法");
    }

    //静态方法
    //可以使用“类名.”方式调用.也可以用“引用.”,即使用的是“引用.”,底层还是用的“类名.”
    //静态方法中不能直接访问非静态数据.
    //静态方法中不能使用this
    public static void m2(){
        //m1();错误,静态方法中不能访问非静态的方法
        //System.out.println(i);错误,静态方法中不能访问非静态的变量
        System.out.println("m2方法");
    }
    public static void main(String[] args){

        StaticTest02 st = new StaticTest02();
        st.m1();
        m2();

        st.m2();//不建议这样使用,因为静态方法跟对象无关
        StaticTest02.m2();//建议这样使用			

        st = null;
        st.m2(); //不会报出空指针异常

    }

}

例3:

public class StaticTest03{
    
    //静态语句块
    static{
        System.out.println("静态语句块1");
    }
    
    static{
        System.out.println("静态语句块2");
    }
    
    static{
        System.out.println("静态语句块3");
    }
    
    //构造方法
    StaticTest03(){
        System.out.println("构造方法");
    }
    
    public static void main(String[] args){
        System.out.println("main main方法 1");
        System.out.println("main main方法 2");
        new StaticTest03();
        new StaticTest03();//静态语句块只执行了一次
    }
    
}

问题:请问下面代码是否可以正常执行?

public class StaticTest04{
    static{
        System.out.println(i);
    }

    static int i = 100;

}

上面代码编译报错,因为静态代码块的执行顺序是自上而下,静态代码块里面打印的变量i还没有声明。

关于static的一些问题

  1. 在java中的main方法就是静态方法,之所以被设计为静态方法的原因就是无需对象,可以直接运行main方法。
  2. 静态方法是不能被重写的,虽然写出重写的代码是可以看到重写的效果,但是加入@Override注解会报错,因此静态方法不能被重写。
  3. 静态方法中不能使用this,this指向当前对象,static又跟对象无关,因此static和this是互斥的。
  4. 静态方法中不能直接访问实例方法和实例变量,后面两者是跟对象有关的。
  5. 静态方法中只能直接访问静态属性和静态方法,实例方法既可以访问静态的属性和方法又能访问非静态的属性和方法。

静态导入

静态导入在实际应用中使用的很少,了解即可。当我想要打印很多内容的时候,会写出下面的代码:

public class StaticImport {
    public static void main(String[] args) {
        System.out.println("我想学习java");
        System.out.println("我想喝口水");
        System.out.println("我想歇一会");
        System.out.println("我想放弃");
    }
}

上面代码中出现了很多System,此时可以使用静态导入简化代码:

import static java.lang.System.out;//静态导入

public class StaticImport {
    public static void main(String[] args) {
        out.println("我想学习java");
        out.println("我想喝口水");
        out.println("我想歇一会");
        out.println("我想放弃");
    }
}

在System类中,out是静态变量,因此我们可以将其进行静态导入,在编码的时候可以直接使用out了。

代码块的分类

什么是代码块?

使用{}括起来的代码被称为代码块,根据其位置和声明的不同可以分为下面4种:

  • 局部代码块,在方法中出现,可以限定变量生命周期。
  • 构造代码块,在类中的方法外的{}叫做构造代码块,晚于静态代码块,优先于构造方法执行,如果在多个构造方法中有相同的代码,可以将这部分代码抽取出来放到构造代码块中。
  • 静态代码块, 在类中方法外出现,并加上static修饰;用于给类进行初始化,在加载的时候就执行,并且只执行一次。一般用于加载驱动。
  • 同步代码块(后面多线程部分会讲解)

例:

public class BlockTest01 {
    public static void main(String[] args) {
        //局部代码块
        {
            int x = 10;						//限定变量的生命周期
            System.out.println(x);
        }
        
        Student s1 = new Student();
        System.out.println("---------------");
        Student s2 = new Student();
    
    }
    
    static {
        System.out.println("main方法类中的静态代码块");
    }
}

class Student {

    public Student(){
        
        System.out.println("构造方法");
    }					

    //构造代码块:每创建一次对象就会执行一次,优先于构造方法执行
    {
        System.out.println("构造代码块");
    }

    static {									//随着类加载而加载,且只执行一次
        System.out.println("静态代码块");	//作用:用来给类进行初始化,一般用来加载驱动
    }											//静态代码块是优先于主方法执行
}

代码块和构造方法执行顺序

下面的标号就是他们的执行顺序

1.静态代码块,随着类初始化而执行,且只执行一次。
2.构造代码块,每创建一个对象就会执行一次,优先于构造方法执行
3.构造方法,每创建一个对象就会执行一次

练习

请问下面代码的运行结果是什么?这个主要考察的是对这些代码块顺序的理解,实际应用中很少会有这样的写法。

class Teacher {
    static {
        System.out.println("Teacher 静态代码块");
    }
    
    {
        System.out.println("Teacher 构造代码块");
    }
    
    public Teacher() {
        System.out.println("Teacher 构造方法");
    }
}

public class TeacherTest {
    static {
        System.out.println("TeacherTest静态代码块");
    }
    
    public static void main(String[] args) {
        System.out.println("main方法");
        
        Teacher t1 = new Teacher();
        Teacher t2 = new Teacher();
    }
}

答案

TeacherTest静态代码块
main方法
Teacher 静态代码块
Teacher 构造代码块
Teacher 构造方法
Teacher 构造代码块
Teacher 构造方法

继承extends

什么是继承?

继承是面向对象三大特征之一。java中的继承描述的是两个类之间的关系,被继承的类称为父类,继承的类称为子类,使用extends关键字来表示。在java语言里面只支持单继承,即一个类只能有一个父类。子类可以继承父类中的非private修饰的成员方法和成员变量,构造方法不能被继承,java里面的继承跟现实生活中的继承颇为相似,现实生活中一个儿子只能有一个父亲,儿子可以继承父亲的房子车子但是不能继承父亲大脑里面的思想和知识。如果一个类没有显示的继承其他类,那么这个类会默认继承Object类,Object是oracle公司提供的java中的根类(老祖宗),也就是说所有java中的类都是Object的子孙。在这之前编写的所有类都是默认继承了Object类。

继承的优点

  • 提高了代码的复用性
  • 增强了代码的可维护性
  • 让类与类之间产生了关系,是多态的前提

继承的缺点
增强了类之间的耦合,原本两个没有关系的类,通过继承使两者发生了关系,这样就产生了耦合。
软件开发的一个原则是高内聚,低耦合。
内聚是一个模块内各个元素彼此结合的紧密程度
耦合是一个软件里面不同模块之间相互连接的数量

举例来说,当不小心将手机摔到地上之后,捡起来发现手机壳裂了,此时只需要将手机壳扔掉换一个新的即可。如果是捡起来之后发下手机屏幕列了,这样就比较麻烦了,需要将手机放到修理店维修。

上面的例子中,手机和手机壳之间的关系耦合度较低,手机和手机屏幕之间的关系耦合度较高。在某些情况下难免会出现耦合,就比如手机和手机屏幕,如果降低耦合也是不方便的。

父类和子类

我们编写程序主要是解决现实生活中的问题,那什么时候定义父类,什么时候定义子类呢?在java中通常会将范围更广的事物定义为父类,比如:

父类:动物,子类:熊猫,老虎,兔子…

父类:水果,子类:西瓜,苹果,葡萄…

父类:蔬菜,子类:白菜,菠菜,黄瓜…

如何使用extends来实现继承关系?

语法:

[修饰符列表] class 子类名 extends 父类名{
    类体;
}

首先,来自己定义一个父类SuperClass

public class SuperClass{
    
    public void m1(){
        System.out.println("SuperClass中的m1方法");
    }
    
    private void m2(){
        System.out.println("SuperClass类中的m2方法");
    }
    
}

然后定义一个子类SubClass来继承SuperClass

public class SubClass extends SuperClass{

    public void m3(){
        System.out.println("SubClass中的m3方法");
    }
}

写一个测试类来测试一下

public class Test01{
    
    public static void main(String[] args){
        
        SubClass s = new SubClass();
        //因为SubClass继承了SuperClass,所以可以在子类里面调用父类的方法
        s.m1();
        
        //s.m2();//子类不能访问父类中private修饰的方法

        //SubClass中自己的方法
        s.m3();
        
    }
    
}

将上面代码修改一下,创建一个SuperClass的父类SuperSuperClass爷爷类

public class SuperSuperClass{
    
    public void m0(){
        System.out.println("SuperSuperClass中的m0方法");
    }
    
}

让SuperClass继承SuperSuperClass

public class SuperClass extends SuperSuperClass{
    
    public void m1(){
        System.out.println("SuperClass中的m1方法");
    }
    
    public void m2(){
        System.out.println("SuperClass类中的m2方法");
    }
    
}

写一个测试类来测试一下

public class Test02{
    
    public static void main(String[] args){
        
        SubClass s = new SubClass();
        s.m0();//可以调用爷爷类中的方法
        
    }
    
}

Java不支持多继承,但是支持多重继承,即子类—>父类—>爷爷类….—>祖先类—>Object类,子类可以直接访问其先辈类里面的非private修饰的方法和属性

在子类中如果要访问父类的private方法或属性的时候,需要在父类中提供get方法,通过get方法间接访问这些内容。

//父类
class Father {
    //父类中的私有属性
    private int age;

    public int getAge() {
        return age;
    }
}

//子类
class Son extends Father{

}

//测试类
class Test03{
    public static void main(String[] args) {
        Son son = new Son();
        son.getAge();//这样就可以利用get方法间接访问父类中private修饰的age了
    }
}

体会继承的优点

比如我要定义一些动物类,兔子,熊猫等等。

//兔子类
class Rabbit{
    public void eat(){
        System.out.println("吃饭");
    }
    public void sleep(){
        System.out.println("睡觉");
    }
}

//熊猫类
class Panda{
    public void eat(){
        System.out.println("吃饭");
    }
    public void sleep(){
        System.out.println("睡觉");
    }
}

上面的代码会有一些问题就是在这两个动物类中出现了一模一样的方法:吃饭和睡觉,这样就产生了冗余,实际开发中要尽量避免出现这种情况,现在就可以使用继承来解决这个问题了。

创建父类Animal,将子类共有的行为定义在这里:

public class Animal{
    public void eat(){
        System.out.println("吃饭");
    }
    public void sleep(){
        System.out.println("睡觉");
    }
}

//让子类继承上面的父类,通过继承来拥有吃饭和睡觉的方法
class Rabbit extends Animal{}
class Panda extends Animal{}

创建测试类:

public class Test{
    public staic void main(String[] args){
        Rabbit r = new Rabbit();
        //使用的是父类种的方法
        r.eat();
        r.sleep();
    }
}

父类中的private变量是否被子类继承?

oracle官方文档中明确的说明了,父类中的private修饰的属性是不能被子类继承的,倘若父类中提供了public或者protected修饰的方法来访问该属性,比如set,get方法,这样在子类中是可以通过方法来使用该private属性的。需要注意的是这里不能被子类继承并非指的是不能被子类使用,父类private属性是会存在于子类对象中的,即存储在该子类对象的堆内存中,注意这里并不会产生父类对象

image-20230706144857487

方法的重写(override)

什么是重写?

重写(override),也叫做覆盖,当父类中的方法无法满足子类需求时,子类可以将父类的方法进行重写来满足需求。比如孩子继承了父亲的房子,可以将房子重新装修。
方法重写的条件:

  • 发生在两个类中,且两个类必须是继承关系
  • 必须具有相同的方法名,相同的返回值类型,相同的参数列表.
  • 子类重写的方法不能比父类被重写的方法拥有更低的访问权限。
  • 子类重写的方法不能比父类被重写的方法抛出更宽泛的异常。(关于异常后面的章节再讲。)
  • 私有的方法不能被重写。
  • 构造方法无法被重写,因为构造方法无法被继承。
  • 静态的方法不存在重写。
  • 重写指的是成员方法,和成员变量无关。

通过下面案例来演示重写。

这里仍然使用上一节定义的Animal,Rabbit,Panda三个类。

public class Animal{
    public void eat(){
        System.out.println("吃饭");
    }
    public void sleep(){
        System.out.println("睡觉");
    }
}

//让子类继承上面的父类,通过继承来拥有吃饭和睡觉的方法
class Rabbit extends Animal{}
class Panda extends Animal{}

上面代码有个问题就是不管是兔子还是熊猫,在执行吃方法的时候都是打印吃饭,现在希望打印出每个动物各自喜爱的食物,让兔子打印吃胡萝卜,熊猫打印出吃竹子,这就是父类方法无法满足子类需求的场景了,可以通过重写来解决这个问题。

class Rabbit extends Animal{
    public void eat(){
        System.out.println("吃胡萝卜");
    }
}
class Panda extends Animal{
    public void eat(){
        System.out.println("吃竹子");
    }
}

创建测试类:

public class AnimalTest01{
    public static void main(String[] args){
        Rabbit r = new Rabbit();
        r.eat();//这里会调用Rabbit重写的方法
        
        Panda p = new Panda();
        p.eat();//这里会调用Panda重写的方法
    }	
}

在执行方法时,java会优先使用类中重写的方法,如果没有重写,再一层一层沿着继承结构向上查找。

注解

建议使用IDEA或其他开发工具来生成重写的方法,在生成的方法上面可以看到一个@Override的东西,这个叫做注解,它的作用是检查当前方法是否满足重写的条件,倘若不满足则会编译报错,通过该注解有利于我们更早的发现问题。后面的学习过程中还会遇到各种各样的注解,不同注解的作用是不一样的,到时候遇到了再进行说明。

public class Panda extends Animal {

    @Override//注解,该注解的作用是检查当前方法是否满足重写的条件
    public void eat() {
        System.out.println("吃竹子");
    }
}

静态方法不存在重写

下面代码虽然从最终的运行结果上来看,是调用的的子类中的静态方法,但这种情况不是重写,静态方法不存在重写。

public class Test01{

    public static void main(String[] args){
        
        A.m1();
        B.m1();
    }
}
class A{
    
    //静态方法
    public static void m1(){
        System.out.println("A中静态方法m1");
    }
}

class B extends A{
    
    //尝试重写父类的静态方法
    public static void m1(){
        System.out.println("B中静态方法m1");
    }
}

super关键字

什么是super?

super代表的是当前子类对象中的父类型特征,需要注意的是super并不是指的父类对象。

super使用场景:

  • 子类和父类中有同名的属性,例如都有name这个属性。在子类中默认会使用其自己的name属性,如果要访问父类中的name属性,需要使用super。例1
  • 子类重写了父类的某个方法(假设这个方法名叫m),如果在子类中需要调用父类中的m方法时,需要使用super。例1
  • 子类调用父类中的构造方法时,需要使用super。例2

注意:super不能用在静态方法中。

例1:

先定义一个Animal类

class Animal {
    
    public String name = "动物";

    public void eat() {				
        System.out.println("吃饭");
    }

    public void sleep() {			
        System.out.println("睡觉");
    }
}

定义一个Rabbit类继承Animal

class Rabbit extends Animal {
        
    public String name = "萌仔";

    public void eat() {				
        System.out.println("吃萝卜");
    }

    public void info(){
        System.out.println(super.name);//获取父类中的name变量
        System.out.println(this.name);//可以不加this,系统默认调用子类自己的name
        super.eat();//调用父类中的eat方法
        this.eat();//调用自己重写的eat方法,可以不加this,默认调用自己的eat方法
        //eat();
    }
}

测试类

public class AnimalTest01 {
    public static void main(String[] args) {
    
        Rabbit r = new Rabbit();
        r.info();
    }
}

例2:

class Animal {
    
    //颜色
    private String color;
    //品种
    private String category;

    //构造方法
    public Animal(){
    
        System.out.println("Animal中的构造方法");
    }

    public Animal(String color,String category){
        
        this.color = color;
        this.category = category;
    }
    
    //get、set方法
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }
}

class Rabbit extends Animal {
        
    public Rabbit(){
        //手动调用父类中的有参构造方法给成员变量进行赋值
        super("白色","长耳兔");
        System.out.println("Rabbit中的构造方法");
    }
}

public class Test {
    public static void main(String[] args) {

        Rabbit r = new Rabbit();
        System.out.println(r.getColor());
        System.out.println(r.getCategory());
    }
}

注意:一个构造方法第一行如果没有this(…);也没有显示的去调用super(…);系统会默认调用super();如果已经有this了,那么就不会调用super了,super(…);的调用只能放在构造方法的第一行,只是调用了父类中的构造方法,但是并不会创建父类的对象。

super和this的对比

  • this和super分别代表什么
    • this:代表当前对象的引用
    • super:代表的是当前子类对象中的父类型特征
  • this和super的使用区别
    • 调用成员变量
      • this.成员变量: 调用本类的成员变量
      • super.成员变量: 调用父类的成员变量
    • 调用构造方法
      • this(…) :调用本类的构造方法
      • super(…):调用父类的构造方法
    • 调用成员方法
      • this.成员方法:调用本类的成员方法
      • super.成员方法:调用父类的成员方法

继承相关面试题

1.请问下面程序的输出结果是什么?

class Fu{
        public int num = 125;
        public Fu(){
            System.out.println("fu");
        }
    }
    class Zi extends Fu{
        public int num = 256;
        public Zi(){
            System.out.println("zi");
        }
        public void show(){
            int num = 512;
            System.out.println(num);
            System.out.println(this.num);
            System.out.println(super.num);
        }
    }
public class Test {
        public static void main(String[] args) {
            Zi z = new Zi();
            z.show();
        }
}

答案:

fu
zi
512
256
125

分析:
创建Zi的对象之后系统会先调用Zi的父类Fu中的构造方法,所以先打印了fu
之后系统调用Zi中的构造方法,打印出zi
在show方法里面有个局部变量num=512,第一行打印num,调用的是局部变量,结果是512
通过this调用了成员变量num,打印出256
通过super调用父类中的num,打印出125

2.请问下面程序输出的结果是什么?

public class Test2_Extends {
    public static void main(String[] args) {
        Zi z = new Zi();
    }

}
class Fu {
    static {
        System.out.println("静态代码块Fu");
    }

    {
        System.out.println("构造代码块Fu");
    }

    public Fu() {
        System.out.println("构造方法Fu");
    }
}

class Zi extends Fu {
    static {
        System.out.println("静态代码块Zi");
    }

    {
        System.out.println("构造代码块Zi");
    }

    public Zi() {
        System.out.println("构造方法Zi");
    }
}

答案:

静态代码块Fu
静态代码块Zi
构造代码块Fu
构造方法Fu
构造代码块Zi
构造方法Zi

分析:
1.系统会先初始化Fu.class和Zi.class,其静态代码块就会执行,所以先打印出了静态代码块中的内容。
2.构造代码块优先于构造方法执行,父类初始化之前,所以打印出父类中的构造代码块和构造方法中的内容。

多态

多态简介

多态就是事物存在的多种形态,比如你在大街上看见一只藏獒,你可以说这只藏獒真凶猛,也可以说这只狗真凶猛,还可以说这个动物真凶猛,以上三种说法其实都是指的这只藏獒。

在Java里面,也是存在多态的,只要符合下面这三种情况,就是多态

  • 有继承
  • 有方法重写
  • 有父类引用指向子类对象

例如下面代码就构成多态

定义一个Animal类

public class Animal{
    
    int num = 10;
    
    public void eat(){
        System.out.println("动物在吃!");
    }
    
}

定义一个Cat类继承Animal

//继承
public class Cat extends Animal{
    
    int num = 20;
    //重写
    public void eat(){
        System.out.println("猫吃猫粮");
    }
    
    //Cat特有的方法
    public void move(){
        System.out.println("猫走路很轻盈!");
    }

}

定义一个Dog类继承Animal

public class Dog extends Animal{

    //重写
    public void eat(){
        System.out.println("狗啃骨头!");
    }
}

上面的三个类里面已经有继承和方法重写了,那么父类引用指向子类对象是什么?请看下面这段代码:
定义一个测试类

public class AnimalTest01{
    
    public static void main(String[] args){
        
        Cat c1 = new Cat();
        c1.eat();
        
        Animal a1 = new Cat();//父类引用指向子类对象,我指着这只猫说这是一只动物
        a1.eat();//会调用Cat中重写的eat方法
        System.out.println(a1.num);//因为成员变量不存在重写,所以结果是10
    }
}

内存图

image-20230706145127183

静态绑定和动态绑定

通过上面示例代码可以看出来,等号左边的内容表示是父类的引用,该引用是在栈内存中,指向了一块堆中的地址,而这个地址中存放的是其子类的对象。上面的代码中,a1是Animal类型的一个引用,指向的是其子类Cat的对象,这个就叫做父类引用指向子类对象。程序在编译(javac)的时候a1被看做Animal类型,所以a1.eat()绑定的是Animal类中的eat()方法,这叫做静态绑定,程序运行时,会在堆中开辟空间创建出对象,此时a1指向的是堆中的Cat对象,而在Cat中对eat()方法进行了重写,所以在运行阶段绑定的是Cat中的eat()方法,这叫做动态绑定

上面总结成一句简短的话就是:编译看左,运行看右。编译的时候看等号左边的类型,运行的时候看等号右边的类型。

强制类型转换

上面代码中父类引用指向子类对象,里面存在着自动类型转换,也叫做向上转型。还有一种情况是父类型向子类型转换,是强制类型转换,也叫向下转型。下面的代码演示了强制类型转换

public class AnimalTest01{
    
    public static void main(String[] args){
                
        Animal a1 = new Cat();//父类引用指向子类对象
        //如果要是想执行Cat里面的move方法该怎么办?
        //只能强制类型转换,需要加强制类型转换符
        Cat c1 = (Cat)a1;
        c1.move();

        Animal a2 = new Dog(); //向上转型.
        //强制类型转换
        //Cat c2 = (Cat)a2; //会报错 java.lang.ClassCastException
        
    }
}

instanceof关键字

上面的代码里面将一个指向Dog对象的Animal引用a2进行强制转换成Cat类型时报出了ClassCastException类转型异常,出现的原因就是堆中的对象是Dog,要将其转换为Cat,这显然是不行的。开发中要是想避免这种错误需要使用instanceof来判断一下。

instanceof语法

引用  instanceof  类型
会判断引用指向的对象是否是该类型的,如果是则计算结果是true,否则结果是false

代码示例

public class AnimalTest01{
    
    public static void main(String[] args){
                
        Animal a1 = new Cat();//父类引用指向子类对象
        //如果要是想执行Cat里面的move方法该怎么办?
        //只能强制类型转换,需要加强制类型转换符
        Cat c1 = (Cat)a1;
        c1.move();

        Animal a2 = new Dog(); //向上转型.
        //进行强制类型转换时,需要先使用instanceof进行判断,避免ClassCastException
        if(a2 instanceof Cat){
            //强制类型转换
            Cat c2 = (Cat)a2;
        }
    }	
}

多态的优点

  • 提高了程序的扩展性
  • 降低了代码之间的耦合

下面两个示例感受下多态的好处。

在返回值中使用多态

使用之前的Animal,Cat,Dog三个类,需求是添加一个方法getAnimal,调用该方法传入整数1时返回一个Dog对象,否则就返回Cat对象。

创建测试类,为了便于main方法直接调用,所以将getAnimal方法声明为static修饰的,另一个问题就是由于方法中会根据实参值的不同返回不同的对象,因此返回值类型不能写具体的Dog或Cat,必须使用Animal,即在返回值的地方存在了父类引用指向子类对象,这里也体现出了多态:

public class TestPoly {
    public static void main(String[] args) {
        Animal a = getAnimal(1);
        a.eat();
    }

    //根据标识,获取动物对象
    public static Animal getAnimal(int i){
        if(i == 1){
            return new Dog();
        }else{
            return new Cat();
        }
    }
}

在方法参数中使用多态

模拟一个人开车换车的例子体会多态的优点:

新建一个Car类

class Car{

    public void run(){
    
        System.out.println("汽车在跑");
    }
}

创建一个Benz类继承Car

class Benz extends Car{
    
    public void run(){
    
        System.out.println("奔驰汽车在跑");
    }
}

创建一个BMW类继承Car

class BMW extends Car {

    public void run(){
    
        System.out.println("宝马汽车在跑");
    }
}

创建一个Person类用来开车,注意下面代码中drive方法的形参Car,在调用drive的时候会传入实参,实参肯定是Car或其子类的对象,所以这里也是多态的写法。

class Person {
    /*
    public void drive(Benz bc){
        bc.run();
    }
    奔驰汽车坏了,再重新创建一个开宝马汽车的方法
    public void drive(BMW bm){
        bm.run();
    }
    */

    //上面代码扩展性太差,每新增加一种品牌的汽车就需要再写一个方法
    //将参数修改为Car类型,这样不论增加什么样的品牌汽车,都可以调用这个方法
    public void drive(Car c){
        c.run();
    }
}

创建一个测试类

public class Test01 {
    public static void main(String[] args) {

        Person james = new Person();

        Benz bc = new Benz();
        james.drive(bc);

        BMW bm = new BMW();	
        james.drive(bm);
    }
}

在工作当中尽量面向抽象编程,不要面向具体编程。在上面实例中drive方法的参数Car相比于Benz和BMW更加抽象一些,就好比生活中,别人会问你会开车么,通常不会问你会开宝马车么?会开奔驰车么?由此可见形参写上Car,程序的扩展性更强了,只要是汽车都能传过来开动。

final关键字

final的特点

final的中文意思是最终,既然是最终就是已经结束了,无法再改变了。在Java里面final关键字同样也有着类似的功能。

  • final修饰的类无法被继承,比如字符串String就是final修饰的,我们不能创建其子类。例1
  • final修饰的方法无法被重写,当某个方法不希望被子类重写的时候,需要将其声明为final修饰。例2
  • final修饰的局部变量,一旦赋值,不可再改变,使用较少。例3
  • final修饰的成员变量必须初始化值,在定义那些不常改变或者永恒不变数据的时候使用,比如定义圆周率,喜马拉雅山的高度等。例4

例1:

final class A{}

class B extends A{}//error无法继承

例2:

class A{
    public final void m1(){}
}

class B extends A{
    public void m1(){}//error无法重写
}

例3:

class A{
    public void m1(){
        
        //声明
        final int i;
        
        //第一次赋值
        i = 100;
        
        //error不能重新赋值
        i = 1200;
        
    }
}

例4:

class A{

    //final修饰的成员变量必须手动初始化.
    final int i = 100;

    //error必须进行初始化
    final int k;
    
    //final修饰的成员变量一般和static联用。
    java规范中要求所有的常量"大写"
    public static final double PI = 3.14;

}

final修饰引用类型

final修饰的引用类型,该引用不可再重新指向其他的java对象。但是final修饰的引用,该引用指向的对象的属性值是可以修改的。

  • 基本类型,是值不能被改变
  • 引用类型,是地址值不能被改变,对象中的属性可以改变
public class FinalTest01{
    
    public static void main(String[] args){
        
        final Customer c = new Customer("张三",20);
        
        //c是final的,无法重新赋值。
        //c = new Customer("李四",21);//Error
        
        c.setName("王五");
        c.setAge(25);
        
        System.out.println(c.getName());
        System.out.println(c.getAge());
        
    }
}

class Customer{
    
    private String name;
    private int age;
    
    Customer(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    //省略set和get方法
    
}

如下图所示,栈中的c是final修饰的,表示c的值是不能改变,它只能是0x11,但是堆中对象的两个属性name和age并未被final修饰,他们的值是可以发生改变的。

image-20230706145216684

抽象类

抽象的概念

抽象这个词说白了就是看不懂,毕加索的画一般都是被称为抽象的。在java里面可以使用关键字abstract修饰一个类,这样的类被称为抽象类,abstract修饰的方法叫做抽象方法。抽象类或抽象方法一般也是看不懂的,因为里面可能根本就没有代码。

抽象类的特点

  • 抽象类无法被实例化,无法创建抽象类的对象。
  • 虽然抽象类没有办法实例化,但是抽象类也有构造方法,该构造方法是给子类创建对象用的。这也算是多态的一种。
  • 抽象类中不一定有抽象方法,但抽象方法必须出现在抽象类中。
  • 抽象类中的子类可以是抽象类,如果不是抽象类的话必须对抽象类中的抽象方法进行重写。
  • 抽象类和抽象方法不能被final修饰

例:

public abstract class A{

    //构造方法
    A(){
        System.out.println("A....");
    }

    //抽象方法
    public abstract void m1();

    public static void main(String[] args){

        //抽象类无法创建对象.
        //A a = new A();

        //多态
        A a = new B();

    }
}


class B extends A{

    public void m1(){

    }

    B(){
        super(); //父类的构造方法虽然调用了,但是并没有创建父类对象。
        System.out.println("B....");
    }

}

抽象类和接口

抽象的概念

抽象这个词说白了就是看不懂,毕加索的画一般都是被称为抽象的。在java里面abstract关键字修饰的方法叫做抽象方法,抽象方法只能存在于抽象类或接口中,抽象方法里面没有方法体,因此得名为抽象。目前市面上服务器端开发的方向,抽象类使用较少,大部分场景下会使用接口,因此抽象类的内容做了解即可。

抽象方法主要是被子类重写的,使用场景会在下面的接口中讲解。

抽象类的特点

  • 抽象类无法被实例化,无法创建抽象类的对象。
  • 虽然抽象类无法被实例化,但是抽象类有构造方法,该构造方法是给子类创建对象用的。这也算是多态的一种。
  • 抽象类中不一定有抽象方法,但抽象方法必须出现在抽象类中。
  • 抽象类中的子类可以是抽象类,如果不是抽象类的话必须对抽象类中的抽象方法进行重写。
  • 抽象类和抽象方法不能被final修饰。

例:

public abstract class A{
    
    //构造方法
    A(){
        System.out.println("A....");
    }
    
    //抽象方法
    public abstract void m1();
    
    public static void main(String[] args){
        
        //抽象类无法创建对象.
        //A a = new A();
        
        //多态
        A a = new B();
        
    }
}

class B extends A{

    public void m1(){
        
    }
    
    B(){
        super(); //父类的构造方法虽然调用了,但是并没有创建父类对象。
        System.out.println("B....");
    }
    
}

接口的概述

电脑上面的主板有很多接口,比如内存条的接口,有了这个接口,可以插入多个内存条,主板和内存条可能不是同一家生产厂商,但是两种物体却能结合到一起,正是因为这个接口的存在。只要两边的厂家都遵循这个接口,主板和内存条就可以随意更换,这么做的好处是提高了可插拔性。试想下,没有这个接口,内存条和主板是连到一体的,倘若内存坏掉了,就需要连同主板一起换掉。

从另一个方面来看,接口主要体现着一种规范,一个项目往往是由多人协作开发完成,有了规范之后,大家都遵循接口中的规范编写代码,这样的代码就不会出现五花八门的情况了。

在java语言里面使用interface来声明一个接口,接口其实是一个特殊的抽象类,jdk8之前的版本中接口里面的方法全部都是抽象的,jdk8新增的特性是允许出现非抽象方法。
关于接口,有几个需要注意的地方:

  • 接口中的变量前面会被默认加上public static final的,即接口中没有变量,都是常量。
  • 接口里面没有构造方法,无法创建接口的对象。
  • 接口和接口之间支持多继承,即一个接口可以有多个父接口。
  • 一个类可以实现多个接口,即一个类可以有多个父接口。
  • 一个类如果实现了接口,那么这个类需要重写接口中所有的抽象方法(建议),如果不重写则这个类需要声明为抽象类(不建议)。
public interface A{
    
    //常量(必须用public static final修饰)
    public static final double PI = 3.14;
    
    //public static final是可以省略的.
    //double PI = 3.14;
    //默认情况下接口中的方法是抽象的
    public abstract void m1();
    
    //public abstract是可以省略的,接口会在方法前自动加上public abstract
    void m2();
    
}

接口之间的多继承:

//定义若干接口
interface B{
    void m1();
}

interface C{
    void m2();
}

interface D{
    void m3();
}

//接口之间可以多继承,接口E可以继承多个父接口
interface E extends B,C,D{
    void m4();
}

一个类可以实现多个接口,使用关键字implements来实现接口,下面代码表示MyClass类实现了两个父接口B和C,由于B和C中有抽象方法,所以需要在实现类MyClass中重写这两个方法。如果不想重写,那就需要将MyClass声明为抽象类。通常情况下,会在实现类中重写接口中的全部抽象方法。

//implements是实现的意思,是一个关键字.
//implements和extends类似。
class MyClass implements B,C{
    public void m1(){}
    public void m2(){}
}

子类在实现接口的同时还可以继承一个父类

class MyClass extends Father implements B,C{
    public void m1(){}
    public void m2(){}
}

接口的作用

  • 可以使项目分层,都面向接口开发,提高开发效率
  • 降低了代码之间的耦合度,提高了代码的可插拔性
  • 定义规范
  • 类型标识
image-20230706145659490

随着学习的深入,这些作用你会慢慢体会到。

由于一个类可以实现多个接口,却只能继承一个父类,所以在开发中大多会使用接口。

通过代码来感受以下接口的作用,将之前的人开汽车的例子修改一下。

将Car定义为接口,因为run方法始终都是要被重写的,所以这里将其声明为抽象方法定义到接口中。

interface Car {
    public void run();
}

创建Benz和BMW类去实现这个接口

class Benz implements Car {
    public void run(){
        System.out.println("奔驰汽车在跑");
    }
}

class BMW implements Car {
    public void run(){
        System.out.println("宝马汽车在跑");
    }
}

Person类不变

class Person {
    public void drive(Car c){
        c.run();
    }
}

测试类不变

public class Test01 {
    public static void main(String[] args) {
        Person james = new Person();
        //接口支持多态
        Car car = new Benz();
        james.drive(car);
    }
}

再次通过代码体会接口的好处

比如你是公司的技术领导,接到任务是要计算不同形状的周长和面积,此时需要你做的事情就是先定义一个形状接口Shape,在这里定义好抽象方法周长和面积。主要作用就是规定好方法名和形参。接下来让你的手下张三和李四根据该接口分别创建长方形和圆形类,正是因为有了接口的规范,所以张三和李四在定义类中的方法具有了一致性。

创建接口Shape:

public interface Shape {
    //求周长
    double getGirth();

    //求面积
    double getArea();
}

创建长方形类:

/*
    张三编写的长方形类
 */
public class Rectangle implements Shape {

    private double height;//长
    private double width;//宽

    public Rectangle(double height, double width) {
        this.height = height;
        this.width = width;
    }

    @Override
    public double getGirth() {
        return 2 * (height + width);
    }

    @Override
    public double getArea() {
        return height * width;
    }

}

创建圆形类:

/*
    李四编写的圆形类
 */
public class Circle implements Shape{
    private double r;//半径

    private static final double PI = 3.1415926;

    public Circle(double r) {
        this.r = r;
    }

    @Override
    public double getGirth() {
        return 2*PI*r;
    }

    @Override
    public double getArea() {
        return PI*r*r;
    }

}

创建测试类:

public class TestShape {
    public static void main(String[] args) {
        //接口支持多态
        Shape circle = new Circle(5);
        System.out.println("圆形的面积是:" + circle.getArea());
        System.out.println("圆形的周长是:" + circle.getGirth());

        //接口支持多态
        Shape rectangle = new Rectangle(5, 6);
        System.out.println("长方形的面积是:" + rectangle.getArea());
        System.out.println("长方形的周长是:" + rectangle.getGirth());
    }
}

default方法

jdk8中新增了default方法,jdk8之前接口中的方法必须都是抽象的,在jdk8中允许接口中定义非抽象方法,在接口中的非抽象方法上使用default修饰即可,default方法的使用方式跟类中的实例方法是一样的。

在接口中定义default方法可以变相的让java支持”多继承”

接口中添加default方法:

public interface MyInterface {
    void m1();
    
    //default方法
    default String m2(String s){return s;};

    //default方法
    default void m3(){
        System.out.println("default方法m3");
    };
}

接口实现类:

public class MyClass implements MyInterface {
    @Override
    public void m1() {
        System.out.println("重写了接口中的m1方法");
    }
}

创建测试类:

public class TestInterface{
    public static void main(String[] args){
        //多态写法:MyInterface mc = new MyClass();

        MyClass mc = new MyClass();
        mc.m1();

        //调用default方法
        System.out.println(mc.m2("default方法"));
        mc.m3();
    }
}

为什么会出现default方法?

java8中打破了接口中方法必须都是抽象的这一规范,有一个好处就是可以提高程序的兼容性,在java.lang.Iterable接口中新增了forEach方法,该方法就是使用default修饰的。试想如果没有default方法,只能在Iterable接口中新增抽象方法,但是这样就会导致java开发者升级jdk版本之后编译报错,因为实现类必须要重写接口中全部的抽象方法,接口一动,所有的实现类都会收到影响。default方法的出现就是为了解决该问题的,实现类可以不用重写接口中的default方法。

接口中的静态方法

在jdk8中还有一项新的特性是可以声明静态方法,使用方式跟类中的静态方法一致。

定义接口:

public interface MyInterface {
    //public可以省略
    public static void m1(){
        System.out.println("静态方法");
    }
}

定义测试类:

public class TestInterface {
    public static void main(String[] args) {
        //使用接口中的静态方法
        MyInterface.m1();
    }
}

Java API简介

什么是API?

API是Application Programming Interface的缩写,中文意思是应用程序编程接口。sun公司提供了一些预先定义好的类和方法以供Java开发者使用,有了API,开发者可以在不查看源代码的情况下了解某个类或者某个方法的使用方式。日常生活中买家用电器时都会带有说明书,里面告诉使用者如何使用,实际上API就像一本说明书。

从哪里可以找到API?

可以在oracle官网里面找到API(jdk8):http://docs.oracle.com/javase/8/docs/api/

jdk11:https://docs.oracle.com/en/java/javase/11/docs/api/index.html
下载离线版的API,这里是CHM格式的:https://pan.baidu.com/s/1skR9wo1 密码:7jiw

如何使用API

这里以离线版的Java API为例,在索引里面输入要查询的类,比如输入一下Object,我们来看下这个Java的根类里面都有哪些方法:

image-20230706145823378 image-20230706145844550

通过查询Java API,我们可以学习很多东西,建议大家在开发中遇到Java提供的不认识的类时候,多去查询一下API,利于各位的技术成长。

Object类中的方法

java.lang.Object是所有java类的根基类(老祖宗),即所有java类都直接或间接是Object的子类,该类中有一些常用的方法,其子类都可以使用,平时在使用对象名.的时候会发现有很多方法的提示,这里面有很多就来自Object类中的方法。

toString方法

Object类中设计toString方法的目的是返回java对象的字符串表示形式。

在现实的开发过程中,Object中的toString方法就是要被重写的,如果没有重写,那么在打印一个引用数据类型的时候,会调用Object中的toString方法。

通过源码可以看到Object类中的toString方法是这样的:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());	
}

Object中的toString方法返回: 类名@哈希算法得出的int类型值再转换成十六进制,为了便于理解,你可以将这个输出结果看做是该java对象在堆中的内存地址。

**如果直接打印一个引用数据类型,系统会默认调用其toString方法。**上面的结果中打印的是对象的内存地址,但有时我们希望打印出该对象中属性的值,此时就是父类方法无法满足子类场景,需要重写toString方法。

创建一个Person类

class Person{
    private String name;
    private int age;
    
    Person(String name,int age){
        this.name = name;
        this.age = age;
    }
    
    //重写toString方法.
    public String toString(){
        return "Person[name="+name+",age="+age+"]";
    }
    
    //set和get略
    
}

创建一个测试类

public class Test01{
    
    public static void main(String[] args){
        
        //创建一个Object类型的对象.
        Object obj = new Object();
        
        //打印结果:java.lang.Object@de6ced,后面这这部分你打印的可能跟我这个不一样
        System.out.println(obj.toString());
        
        //创建一个Person类型的对象
        Person p1 = new Person("刘德华",54);
        System.out.println(p1.toString()); 
        Person p2 = new Person("郭富城",50);
        System.out.println(p2.toString()); 
        
        //print方法后面括号中如果是一个引用类型,会默认调用引用类型的toString方法.
        //下面的写法跟上面调用toString是一样的
        System.out.println(p1); 
        System.out.println(p2); 
    }
}

equals方法

java对象中的equals方法的设计目的:判断两个对象内容是否一样,通常一个对象中会有很多属性,在比较两个对象是否相等的时候,一般是看这些属性的值是否相等,所以需要通过调用其equals方法进行比较,需要注意的是要重写equals方法,在里面编写比较规则,如果没有重写的话,默认会调用Object中的equals方法,源码如下:

    public boolean equals(Object obj) {
        return (this == obj);
    }

Object中的equals方法使用的是==进行的比较,实际上是比较的两个对象的内存地址,地址相同则是true,否则则false。并不会比较里面的属性,如果要比较两个对象中的属性是否一致,需要重写equals方法了。

当你希望比较两个对象中的属性是否一致时,要重写equals方法,如果你希望比较两个对象的内存地址是否相同时,使用==即可。

下面创建一个Star明星类,通常我们认为两个对象中的属性内容全部相等时,这两个对象就是相等的,所以在Star类里面重写了equals方法,比较了两个属性id和name。

class Star{
    
    //编号
    private int id;
    
    //姓名
    private String name;
    
    public Star(int id,String name){
        this.id = id;
        this.name = name;
    }
    
    //根据需求规定重写equals方法,两个Star对象的id和name都相等则认为这两个对象是一样的
    //s1.equals(s2);
    public boolean equals(Object obj){
        
        if(this==obj){
            return true;
        } 
        //注意强转之前先做个判断,否则可能会出现ClassCastException
        if(obj instanceof Star){
            Star s = (Star)obj;
            //String已经重写了equals方法,这里可直接使用
            if(s.id == id && s.name.equals(name)){
                return true;
            }
        }
        
        return false;
    }
    
    //set,get略
}

创建一个测试类:

public class Test01{
    
    public static void main(String[] args){
        
        Object o1 = new Object();
        Object o2 = new Object();
        
        boolean b1 = o1.equals(o2);
        
        System.out.println(b1); //false
        Star s1 = new Star(100,"成龙");
        Star s2 = new Star(100,"成龙");
        Star s3 = new Star(110,"李连杰");
        
        System.out.println(s1.equals(s2)); //true
        System.out.println(s2.equals(s3)); //false
    }
}

比较两个String类型的值时,不要使用==,要使用equals方法,String已经重写了Object中的equals方法,这里比较的是String的内容。

public class Test02{
    
    public static void main(String[] args){
        
        String s1 = new String("ABC");
        String s2 = new String("ABC");
        
        System.out.println(s1==s2); //false
        
        //String已经重写了Object中的equals方法,比较的是内容。
        System.out.println(s1.equals(s2)); //true
        
    }
}

Objects工具类

java.util包下有一个Objects工具类,它提供了一些方法来操作对象,由一些静态方法组成,这些方法是null-save(空指针安全的),可以用这个方法比较两个对象,此时可以避免空指针问题。

在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下:

  • public static boolean equals(Object a, Object b):判断两个对象是否相等。

通过其源码可以看到其中有非空的判断。

hashCode方法

调用该方法会返回一个整数,我们称之为哈希值,通过源码可以看到,hashCode方法是native关键字修饰的,表示该方法是调用了c/c++语言的实现。通常情况下在重写equals方法的同时会重写hashCode,不需要我们自己手动重写,通过idea等开发环境生成即可。接下来先说一下hashCode方法的作用。

比如有一个书架分为3层,每层有100本书籍,总共300本,这些书籍没有重复的,现在需要将一本新的书籍《java编程思想》放入,在保证书架上所有图书都不重复的前提下,需要用这本书分别与书架上的300图书进行比较(从java角度上来看就是调用equals方法),最差的情况是比300次,这种做法显然是低效的。

如何解决目前低效的问题呢?实际情况中,往往会对书架上的书籍进行分类,假设三层分别存放文学类,计算机类,动漫类的图书,各有100本,总共300本:

image-20230706145938212

分类之后,再次放入《java编程思想》这本书的时候,只需要跟第二层的100本书籍比较,最差的情况是比100次,这显然要比之前的300次效率提升了不少。

书架上每层的分类就好比是哈希值,当然,哈希值是整数,这里的分类是字符串,我们只是做个类比。调用对象的hashCode方法时会获取一个哈希值,不同的对象有可能是相同的哈希值(好比是上面不同的书籍有相同的分类),把具有相同哈希值的对象放到一起,在进行equals比较之前先计算其哈希值,然后再与这些具有相同哈希值的对象进行equals比较,效率会有显著的提升。在后面集合部分就是使用的这种方式。

如果两个对象的哈希值相等,那么equals有可能不等。

如果两个对象的equals相等,那么他们的哈希值肯定相等。

下面代码演示使用了idea生成的hashCode方法

import java.util.Objects;

public class Student {

    private int id;
    private String name;

    //生成的hash
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return id == student.id &&
                Objects.equals(name, student.name);
    }

    public Student() {
    }

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

生成的hashCode方法中查看其源码可以发现,最终计算的哈希值是跟当前对象的属性值id和name有关。在计算哈希值的时候其内部使用了一个数字31,这个数字是前辈们经过多次的实验找出的,通过31可以让对象计算出的哈希值更加均匀的分散,试想下如果调用100个对象上的hashCode方法,计算出的哈希值一样,那这样的hashCode方法并未起到作用。

getClass方法

万物皆对象,jvm会将字节码文件加载到其内部一个叫做方法区的地方,然后会在堆中创建这个字节码文件对应的对象,这个对象是java.lang.Class类型的,在jvm中每个字节码文件只有一个对应的Class对象,我们可以通过Object中提供的getClass方法来获取这个对象。这个对象主要会在后面反射章节中使用,目前只需要理解这个字节码文件对象是什么即可。

image-20230706150000833

示例代码,在Class类中已经重写了toString方法,所以在打印其对象时,我们会看到类的名字。目前来看,可以使用getClass方法判断两个对象是否为相同的类型:

class Animal{}
class Dog extends Animal{}
class Cat extends Animal{}

//测试类
public class TestObject {
    public static void main(String[] args) {
        Animal a = new Cat();
        Animal b = new Dog();
        Animal c = new Dog();
        
        //获取Cat对应的Class对象
        System.out.println(a.getClass());
        获取Dog对应的Class对象
        System.out.println(b.getClass());
        
        //判断两个类型是否一致
        System.out.println(a.getClass() == b.getClass());
        System.out.println(a.getClass() == c.getClass());

    }

}

Object中除了上面介绍的方法还有一些其他的,我们后续用到的时候再来讲解。Object中的finalize()方法已在jdk9中废弃,这里就不再介绍了。

废弃

在Object类中的finalize方法中可以看到是有一个横线的,该方法上面有个注解@Deprecated(since=”9″),表示其在jdk9版本中废弃掉了。废弃的方法是可以正常使用的,但是不建议使用了。随着时间的推移,早期java提供的类里面的方法可能会导致一些问题,这些方法通常不会被删掉,而是加上@Deprecated注解将其废弃。

native关键字

查看java源码的时候会发现有的方法是native修饰的,表示该方法是调用其他语言编写的实现。native的出现主要是由于某些层次使用java语言不易编写或效率不高,此时可以使用其他语言实现,然后再通过java来进行调用。

Object类之equals方法

equals方法

java对象中的equals方法的设计目的:判断两个对象内容是否一样,通常一个对象中会有很多属性,在比较两个对象是否相等的时候,一般是看这些属性的值是否相等,所以需要通过调用其equals方法进行比较,需要注意的是先重写equals方法,里面编写比较规则,如果没有重写的话,默认会调用Object中的equals方法,源码如下:

    public boolean equals(Object obj) {
        return (this == obj);
    }

如果使用==比较引用类型,那么比较的是两个对象的内存地址,地址相同则是true,反之则false.

Object中的equals方法比较的是两个引用的内存地址。但是在现实的业务逻辑当中,不应该比较内存地址,应该比较地址里面的内容,所以需要对equals方法进行重写。

创建一个Star类

class Star{
    
    //身份证号
    int id;
    
    //姓名
    String name;
    
    public Star(int id,String name){
        this.id = id;
        this.name = name;
    }
    
    //根据需求规定重写equals方法,两个Star对象的id和name都相等则认为这两个对象是一样的
    //s1.equals(s2);
    public boolean equals(Object obj){
        
        if(this==obj){
            return true;
        } 
        
        if(obj instanceof Star){
            Star s = (Star)obj;
            if(s.id == id && s.name.equals(name)){
                return true;
            }
        }
        
        return false;
    }
    
}

创建一个测试类:

public class Test01{
    
    public static void main(String[] args){
        
        Object o1 = new Object();
        Object o2 = new Object();
        
        boolean b1 = o1.equals(o2);
        
        System.out.println(b1); //false
        Star s1 = new Star(100,"成龙");
        Star s2 = new Star(100,"成龙");
        Star s3 = new Star(110,"李连杰");
        
        System.out.println(s1.equals(s2)); //true
        System.out.println(s2.equals(s3)); //false
    }
}

比较两个String类型时,不能使用==,要使用equals方法,String已经重写了Object中的equals方法,比较的是内容。

public class Test02{
    
    public static void main(String[] args){
        
        String s1 = new String("ABC");
        String s2 = new String("ABC");
        
        System.out.println(s1==s2); //false
        
        //String已经重写了Object中的equals方法,比较的是内容。
        System.out.println(s1.equals(s2)); //true
        
    }
}

Objects工具类

java.util包下有一个Objects工具类,它提供了一些方法来操作对象,由一些静态的实用方法组成,这些方法是null-save(空指针安全的),可以用这个方法比较两个对象,此时可以避免空指针问题。

在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下:

  • public static boolean equals(Object a, Object b):判断两个对象是否相等。

Object类之finalize方法

finalize方法

finalize方法不需要程序员去调用,由系统自动调用。java对象如果没有更多的引用指向它,则该java对象成为垃圾数据,等待垃圾回收器的回收,垃圾回收器在回收这个java对象之前会自动调用该对象的finalize方法。finalize方法是该对象马上就要被回收了,例如:需要释放资源,则可以在该方法中释放。

public class Test01{

    public static void main(String[] args){

        Person p1 = new Person();

        p1 = null; //没有引用再指向它.等待回收.

        //程序员只能“建议”垃圾回收器回收垃圾.
        System.gc();
    }

}


class Person{

    //重写Object中的finalize方法.
    public void finalize() throws Throwable { 

        System.out.println(this + "马上就要被回收了!");

        //让引用再次重新指向该对象,该对象不是垃圾数据,不会被垃圾回收器回收!
        //Person p = this;
    }
}

package(包)和import

包的概念

在日常生活中有很多同名的人,为了将这些同名的人进行区分,就出现了身份证,每个人的身份证号都是不一样的。在Java语言里面,开发者难免会编写出同名的类,为了区分出不通人开发出来的类,Java引入了包的概念。

使用package声明包名

在类名前面使用关键字package加入包名来避免命名冲突问题,因为域名是世界上唯一的,所以建议使用公司倒写的域名来命名包名,通常是小写
例如:package com.monkey1024.score.system
上面包名的含义是monkey1024公司开发的score项目(学生成绩管理项目),system是score项目里面的一个模块。
假设这个score项目里面有学生模块、老师模块,可以这样进行命名:
学生模块:com.monkey1024.score.student
在学生模块的包里面,可以放置一些学生相关的类,比如AddStudent.class、DeleteSudent.class
老师模块:com.monkey1024.score.teacher
在老师模块的包里面,可以放置一些老师相关的类,比如AddTeacher.class、DeleteTeacher.class
其实这个包名就是文件夹的名称,如果按照上述命名,假设在我存放在电脑的f盘里面,F:\com\monkey1024\score\student\AddStudent.class

注意:

  • package语句只能出现在.java源文件的第一行
  • package语句在一个java文件中只能有一个
  • 如果没有package,默认表示无包名

创建一个添加学生类:

package com.monkey1024.oa.student;

public class AddStudent{
    public void add(){
        System.out.println("添加学生");
    }
}    

创建一个测试类

package com.monkey1024.oa.student;
public class Test01{

    public static void main(String[] args){
        AddStudent as = new AddStudent();
        as.add();
        System.out.println(as); 
    }
}

带包类的编译和运行

使用javac命令编译时,加上-d
例如:javac -d . HelloWorld.java
上面的.表示当前路径

运行时,使用java 包名.HelloWorld
需要加上包名

使用import关键字导入不同包下的类

将上面的Test01的包名修改一下

    package com.monkey1024.oa.system;

    public class Test01{

        public static void main(String[] args){
            AddStudent as = new AddStudent();//报错找不到类
            as.add()
            System.out.println(as); 
        }
    }

上面代码将会报错,因为两个类在不同的包里面,在Test01这个包里面,系统找不到AddStudent类,所以前面需要加上包名:

com.monkey1024.oa.student.AddStudent as = new com.monkey1024.oa.student.AddStudent();

每次用到这个类时都需要写上包名,比较繁琐,我们可以使用import关键字将不同包下的类导入

    package com.monkey1024.oa.system;

    import com.monkey1024.oa.student.*//导入这个包下的所有类
    import com.monkey1024.oa.student.AddStudent//导入这个包下的AddStudent类,建议使用这种方式

    public class Test01{

        public static void main(String[] args){
            AddStudent as = new AddStudent();//这样就没问题了
            as.add()
            System.out.println(as); 
        }
    }

注意:java.lang软件包下所有类不需要手动导入,系统自动导入,Object类,String类都在这个包里面

访问控制权限

4种访问控制权限

java访问级别修饰符主要包括:private 、protected、public和default(默认,注意这里的default不是关键字,而是表示什么都不写),可以限定其他类对该类、属性和方法的使用权限。

修饰词本类同一个包的类子类任何地方
private×××
default(默认)××
protected×
public

注意以上对类的修饰只有:public和default,内部类除外

priavte和public都比较好理解和记忆,这里就不演示了,主要演示一下不同包下的两个具有父子关系的类里面使用protected和default的区别。

创建一个Person类

package com.monkey1024.score.sys;

public class Person{

    String name;

    protected int age;

    void m1(){
        System.out.println("m1");
    }

    protected void m2(){
        System.out.println("m2");
    }
}

创建一个User类,与Person类不在同一个包下

package com.monkey1024.score.buss;

import com.monkey1024.score.sys.Person;
public class User extends Person{

    public void m3(){

        m1();//无法访问,因为父类里面是default修饰的
        m2();
        System.out.println(age);
        System.out.println(name);//无法访问,因为父类里面是default修饰的
    }
}

内部类

内部类的分类

内部类,顾名思义就是在一个类的内部声明一个类。内部类主要分为:

  • 静态内部类
  • 匿名内部类
  • 成员内部类
  • 局部内部类

内部类的特点:
内部类可以直接访问外部类的成员,包括private修饰的变量和方法

在编写swing程序时内部类出现的频率比较高,不过目前大部分软件公司基本都不会使用swing,所以本节内容简单了解即可。

静态内部类

1.静态内部类可以等同看做静态变量
2.静态内部类可以直接访问外部类的静态数据

public class OuterClass{

    //静态变量
    private static String s1 = "A";

    //成员变量
    private String s2 = "B";

    //静态方法
    private static void m1(){
        System.out.println("静态方法m1");
    }

    //成员方法
    private void m2(){
        System.out.println("m2方法");
    }

    //静态内部类
    //可以用访问控制权限的修饰符修饰。
    //public,protected,private,缺省
    static class InnerClass{

        //静态方法
        public static void m3(){
            System.out.println(s1);
            m1();
        }

        //成员方法
        public void m4(){
            System.out.println(s1);
            m1();
        }
    }


    public static void main(String[] args){

        //执行m3
        OuterClass.InnerClass.m3();

        //执行m4
        InnerClass inner = new InnerClass();
        inner.m4();

    }

}

成员内部类

1.成员内部类可以等同看做成员变量
2.成员内部类中不能有静态声明
3.成员内部类可以访问外部类所有的数据

public class OuterClass{

    //静态变量
    private static String s1 = "A";

    //成员变量
    private String s2 = "B";

    //静态方法
    private static void m1(){
        System.out.println("静态方法m1");
    }

    //成员方法
    private void m2(){
        System.out.println("m2方法");
    }

    //成员内部类
    //可以用访问控制权限的修饰符修饰。
    //public,protected,private,缺省
    class InnerClass{

        //静态方法
        //public static void m3(){}

        //成员方法
        public void m4(){
            System.out.println(s1);
            m1();

            System.out.println(s2);
            m2();
        }
    }

    public static void main(String[] args){

        //创建外部类对象
        OuterClass oc = new OuterClass();
        InnerClass inner = oc.new InnerClass();
        inner.m4();
    }
}

匿名内部类

一个没有名字的类就叫做匿名内部类

public class Test{

    //静态方法
    public static void m1(Animal a){
        a.eat();
    }

    //入口
    public static void main(String[] args){


        //使用匿名内部类的方式执行m1方法
        //整个这个"new Animal(){}"就是个匿名内部类
        m1(new Animal(){
            public void eat(){
                System.out.println("吃猫粮");
            }
        });

        //匿名内部类的优点:少定义一个类.
        //缺点:无法重复使用!
    }

}

//接口
interface Animal{
    void eat();
}

局部内部类

局部内部类在访问局部变量的时候,局部变量必须使用final修饰,在JDK8之后,这个限制被取消了

public class OuterClass{

    //方法
    public void m1(){

        //局部变量
        final int i = 10;

        //局部内部类
        //局部内部类不能用访问控制权限修饰符修饰。
        class InnerClass{

            //内部类不能有静态声明
            //public static void m1(){}

            //成员方法
            public void m2(){
                System.out.println(i); // 10
            }

        }

        //调用m2
        InnerClass inner = new InnerClass();
        inner.m2();
    }

    public static void main(String[] args){
        OuterClass oc = new OuterClass();
        oc.m1();
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值