Java基础笔记整理(一)

在此感谢黑马程序员的视频教程
获取更好的阅读体验请前往我的博客主站!
如果本文对你的学习有帮助,请多多点赞、评论、收藏,你们的反馈是我更新最大的动力!

面向对象高级(一)

一、静态

1.1 static修饰成员变量

Java中的成员变量按照有无static修饰分为两种:类变量、实例变量。它们的区别如下图所示:

1663977705413

由于静态变量是属于类的,只需要通过类名就可以调用:类名.静态变量

实例变量是属于对象的,需要通过对象才能调用:对象.实例变量

  • 代码的内存原理

1663978808670

  • 最后总结一下
- 1.类变量:属于类,在内存中只有一份,用类名调用
- 2.实例变量:属于对象,每一个对象都有一份,用对象调用

1.2 static修饰成员变量的应用场景

在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成员变量来记住。

我们看一个案例

需求:系统启动后,要求用于类可以记住自己创建了多少个用户对象。**

  • 第一步:先定义一个User类,在用户类中定义一个static修饰的变量,用来表示在线人数;
public class User{
    public static int number;
    //每次创建对象时,number自增一下
    public User(){
        User.number++;
    }
}
  • 第二步:再写一个测试类,再测试类中创建4个User对象,再打印number的值,观察number的值是否再自增。
public class Test{
    public static void main(String[] args){
        //创建4个对象
        new User();
        new User();
        new User();
        new User(); 
        
        //查看系统创建了多少个User对象
        System.out.println("系统创建的User对象个数:"+User.number);
    }
}

运行上面的代码,查看执行结果是:系统创建的User对象个数:4

1.3 static修饰成员方法

  • 成员方法根据有无static也分为两类:类方法、实例方法

1664004813041

有static修饰的方法,是属于类的,称为类方法;调用时直接用类名调用即可。

无static修饰的方法,是属于对象的,称为实例方法;调用时,需要使用对象调用。

我们看一个案例,演示类方法、实例方法的基本使用

  • 先定义一个Student类,在类中定义一个类方法、定义一个实例方法
public class Student{
    double score;
    
    //类方法:
    public static void printHelloWorld{
        System.out.println("Hello World!");
        System.out.println("Hello World!");
    }
    
    //实例方法(对象的方法)
    public void printPass(){
        //打印成绩是否合格
        System.out.println(score>=60?"成绩合格":"成绩不合格");
    }
}
  • 在定义一个测试类,注意类方法、对象方法调用的区别
public class Test2{
    public static void main(String[] args){
        //1.调用Student类中的类方法
        Student.printHelloWorld();
        
        //2.调用Student类中的实例方法
        Student s = new Student();        
        s.printPass();
        
        //使用对象也能调用类方法【不推荐,IDEA连提示都不给你,你就别这么用了】
        s.printHelloWorld();
    }
}
  • static修饰成员方法的内存原理。
1.类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的;
		 所以类名直接就可以找到static修饰的方法
		 
2.实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。

1664005554987

1.4 工具类

如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。

  • 我们写一个生成验证码的工具类
public class MyUtils{
    public static String createCode(int n){
        //1.定义一个字符串,用来记录产生的验证码
        String code = "";
        
        //2.验证码是由所有的大写字母、小写字母或者数字字符组成
        //这里先把所有的字符写成一个字符串,一会从字符串中随机找字符
        String data = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ";
        
        //3.循环n次,产生n个索引,再通过索引获取字符
        Random r = new Random();
        for(int i=0; i<n; i++){
            int index = r.nextInt(data.length());
            char ch = data.charAt(index);
            //4.把获取到的字符,拼接到code验证码字符串上。
            code+=ch;
        }
        
        //最后返回code,code的值就是验证码
        return code;
    }
}
  • 接着可以在任何位置调用MyUtilscreateCOde()方法产生任意个数的验证码
//比如这是一个登录界面
public class LoginDemo{
    public static void main(String[] args){
        System.out.println(MyUtils.createCode());
    }
}
//比如这是一个注册界面
public class registerDemo{
    public static void main(String[] args){
        System.out.println(MyUtils.createCode());
    }
}

在补充一点,工具类里的方法全都是静态的,推荐用类名调用为了防止使用者用对象调用。我们可以把工具类的构造方法私有化。

public class MyUtils{
    //私有化构造方法:这样别人就不能使用构造方法new对象了
    private MyUtils(){
        
    }
    
    //类方法
    public static String createCode(int n){
       ...
    }
}

1.5 static的注意事项

1664007168869

public class Student {
    static String schoolName; // 类变量
    double score; // 实例变量

    // 1、类方法中可以直接访问类的成员,不可以直接访问实例成员。
    public static void printHelloWorld(){
        // 注意:同一个类中,访问类成员,可以省略类名不写。
        schoolName = "学校";
        printHelloWorld2();

        System.out.println(score); // 报错的
        printPass(); // 报错的

        ystem.out.println(this); // 报错的
    }
    
	// 类方法
    public static void printHelloWorld2(){

    }
    
    // 实例方法
    public void printPass2(){

    }
    
    // 实例方法
    // 2、实例方法中既可以直接访问类成员,也可以直接访问实例成员。
    // 3、实例方法中可以出现this关键字,类方法中不可以出现this关键字的
    public void printPass(){
        schoolName = "学校2"; //对的
        printHelloWorld2(); //对的

        System.out.println(score); //对的
        printPass2(); //对的

        System.out.println(this); //对的
    }
}

1.6 static应用(代码块)

代码块根据有无static修饰分为两种:静态代码块、实例代码块

静态代码块:

1664007549583

public class Student {
    static int number = 80;
    static String schoolName = "黑马";
    // 静态代码块
    static {
        System.out.println("静态代码块执行了~~");
        schoolName = "黑马";
    }
}

静态代码块不需要创建对象就能够执行

public class Test {
    public static void main(String[] args) {
        // 目标:认识两种代码块,了解他们的特点和基本作用。
        System.out.println(Student.number);
        System.out.println(Student.number);
        System.out.println(Student.number);

        System.out.println(Student.schoolName); // 黑马
    }
}

执行上面代码时,发现没有创建对象,静态代码块就已经执行了。

1664007747151

关于静态代码块重点注意:静态代码块,随着类的加载而执行,而且只执行一次。

实例代码块

1664008215853

实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。

public class Student{
    //实例变量
	int age;
    //实例代码块:实例代码块会执行在每一个构造方法之前
    {
        System.out.println("实例代码块执行了~~");
        age = 18;
        System.out.println("有人创建了对象:" + this);
    }

    public Student(){
        System.out.println("无参数构造器执行了~~");
    }

    public Student(String name){
        System.out.println("有参数构造器执行了~~");
    }
}

接下来在测试类中进行测试,观察创建对象时,实例代码块是否先执行了。

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student();
        Student s2 = new Student("张三");
        System.out.println(s1.age);
        System.out.println(s2.age);
    }
}

对于实例代码块重点注意:实例代码块每次创建对象之前都会执行一次

二、继承

2.1 继承

1664009338913

接下来,我们演示一下使用继承来编写代码,注意观察继承的特点。

public class A{
    //公开的成员
    public int i;
    public void print1(){
        System.out.println("===print1===");
    }
    
    //私有的成员
    private int j;
    private void print2(){
        System.out.println("===print2===");
    }
}

然后,写一个B类,让B类继承A类。在继承A类的同时,B类中新增一个方法print3

public class B extends A{
    public void print3(){
        //由于i和print1是属于父类A的公有成员,在子类中可以直接被使用
        System.out.println(i); //正确
        print1(); //正确
        
        //由于j和print2是属于父类A的私有成员,在子类中不可以被使用
        System.out.println(j); //错误
        print2();
    }
}
  • 继承的内存原理

这里我们只需要关注一点:子类对象实际上是由子、父类两张设计图共同创建出来的。

所以,在子类对象的空间中,既有本类的成员,也有父类的成员。但是子类只能调用父类公有的成员。

1664010590126

2.2 继承的好处

1664010915416

观察代码发现,我们会发现Teacher类中和Consultant类中有相同的代码;其实像这种两个类中有相同代码时,没必要重复写。

我们可以把重复的代码提取出来,作为父类,然后让其他类继承父类就可以了,这样可以提高代码的复用性。改造后的代码如下:

1664011136599

接下来使用继承来完成上面的案例,这里只演示People类和Teacher类,然后你尝试自己完成Consultant类。

  • 先写一个父类 People,用来设计Teacher和Consultant公有的成员。
public class People{
    private String name;
    
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name=name;
    }
}
  • 再写两个子类Teacher继承People类,同时在子类中加上自己特有的成员。
public class Teacher extends People{
    private String skill; //技能
    
    public String getSkill(){
        return skill;
    }
    
    public void setSkill(String skill){
        this.skill=skill;
    }
    
    public void printInfo(){
        System.out.println(getName()+"具备的技能:"+skill);
    }
}
  • 最后再写一个测试类,再测试类中创建Teacher、Consultant对象,并调用方法。
public class Test {
    public static void main(String[] args) {
        // 目标:搞清楚继承的好处。
        Teacher t = new Teacher();
        t.setName("播仔");
        t.setSkill("Java、Spring");
        System.out.println(t.getName());
        System.out.println(t.getSkill());
        t.printInfo();
    }
}

执行代码,打印结果如下:

1664011737379

关于继承的好处我们只需要记住:继承可以提高代码的复用性

2.3 权限修饰符

什么是权限修饰符呢?

权限修饰符是用来限制类的成员(成员变量、成员方法、构造器…)能够被访问的范围。

每一种权限修饰符能够被访问的范围如下

1664012151488

下面我们用代码演示一下,在本类中可以访问到哪些权限修饰的方法。

public class Fu {
    // 1、私有:只能在本类中访问
    private void privateMethod(){
        System.out.println("==private==");
    }

    // 2、缺省:本类,同一个包下的类
    void method(){
        System.out.println("==缺省==");
    }

    // 3、protected: 本类,同一个包下的类,任意包下的子类
    protected void protectedMethod(){
        System.out.println("==protected==");
    }

    // 4、public: 本类,同一个包下的类,任意包下的子类,任意包下的任意类
    public void publicMethod(){
        System.out.println("==public==");
    }

    public void test(){
        //在本类中,所有权限都可以被访问到
        privateMethod(); //正确
        method(); //正确
        protectedMethod(); //正确
        publicMethod(); //正确
    }
}

接下来,在和Fu类同一个包下,创建一个测试类Demo,演示同一个包下可以访问到哪些权限修饰的方法。

public class Demo {
    public static void main(String[] args) {
        Fu f = new Fu();
        // f.privateMethod();	//私有方法无法使用
        f.method();
        f.protectedMethod();
        f.publicMethod();
    }
}

接下来,在另一个包下创建一个Fu类的子类,演示不同包下的子类中可以访问哪些权限修饰的方法。

public class Zi extends Fu {
    //在不同包下的子类中,只能访问到public、protected修饰的方法
    public void test(){
        // privateMethod(); // 报错
        // method(); // 报错
        protectedMethod();	//正确
        publicMethod();	//正确
    }
}

接下来,在和Fu类不同的包下,创建一个测试类Demo2,演示一下不同包的无关类,能访问到哪些权限修饰的方法;

public class Demo2 {
    public static void main(String[] args) {
        Fu f = new Fu();
        // f.privateMethod(); // 报错
        // f.method();		  //报错
        // f.protecedMethod();//报错
        f.publicMethod();	//正确

        Zi zi = new Zi();
        // zi.protectedMethod();
    }
}

2.4 单继承、Object

Java语言只支持单继承,不支持多继承,但是可以多层继承。就像家族里儿子、爸爸和爷爷的关系一样:一个儿子只能有一个爸爸,不能有多个爸爸,但是爸爸也是有爸爸的。

1664016601977

public class Test {
    public static void main(String[] args) {
        // 目标:掌握继承的两个注意事项事项。
        // 1、Java是单继承的:一个类只能继承一个直接父类;
        // 2、Object类是Java中所有类的祖宗。
        A a = new A();
        B b = new B();

        ArrayList list = new ArrayList();
        list.add("java");
        System.out.println(list.toString());
    }
}

class A {} //extends Object{}
class B extends A{}
// class C extends B , A{} // 报错
class D extends B{}

2.5 方法重写

什么是方法重写

当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。

注意:重写后,方法的访问遵循就近原则。下面我们看一个代码演示

写一个A类作为父类,定义两个方法print1和print2

public class A {
    public void print1(){
        System.out.println("111");
    }

    public void print2(int a, int b){
        System.out.println("111111");
    }
}

再写一个B类作为A类的子类,重写print1和print2方法。

public class B extends A{
    // 方法重写
    @Override // 安全,可读性好
    public void print1(){
        System.out.println("666");
    }


    // 方法重写
    @Override
    public void print2(int a, int b){
        System.out.println("666666");
    }
}

接下来,在测试类中创建B类对象,调用方法

public class Test {
    public static void main(String[] args) {
        // 目标:认识方法重写,掌握方法重写的常见应用场景。
        B b =  new B();
        b.print1();
        b.print2(2, 3);
    }
}

执行代码,我们发现真正执行的是B类中的print1和print2方法

1664149862001

知道什么是方法重写之后,还有一些注意事项,需要和大家分享一下。

- 1.重写的方法上面,可以加一个注解@Override,用于标注这个方法是复写的父类方法
- 2.子类复写父类方法时,访问权限必须大于或者等于父类方法的权限
	public > protected > 缺省
- 3. 重写的方法返回值类型,必须与被重写的方法返回值类型一样,或者范围更小
- 4. 私有方法、静态方法不能被重写,如果重写会报错。

关于这些注意事项,同学们其实只需要了解一下就可以了。实际上我们实际写代码时,只要和父类写的一样就可以( 总结起来就8个字:声明不变,重新实现

方法重写的应用场景

方法重写的应用场景之一就是:子类重写Object的toString()方法,以便返回对象的内容。

比如:有一个Student类,这个类会默认继承Object类。

public class Student extends Object{
    private String name;
    private int age;

    public Student() {
    }

    public Student(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;
    }
}

其实Object类中有一个toString()方法,直接通过Student对象调用Object的toString()方法,会得到对象的地址值。

public class Test {
    public static void main(String[] args) {
        Student s = new Student("播妞", 19);
        // System.out.println(s.toString());
        System.out.println(s);
    }
}

1664150713665

但是,此时不想调用父类Object的toString()方法,那就可以在Student类中重新写一个toSting()方法,用于返回对象的属性值。

package com.itheima.d12_extends_override;

public class Student extends Object{
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

重新运行测试类,结果如下

1664150752636

2.6 子类中访问成员的特点

  • 原则:在子类中访问其他成员(成员变量、成员方法),是依据就近原则的

定义一个父类,代码如下

public class F {
    String name = "父类名字";

    public void print1(){
        System.out.println("==父类的print1方法执行==");
    }
}

再定义一个子类,代码如下。有一个同名的name成员变量,有一个同名的print1成员方法;

public class Z extends F {
    String name = "子类名称";
    public void showName(){
        String name = "局部名称";
        System.out.println(name); // 局部名称
    }

    @Override
    public void print1(){
        System.out.println("==子类的print1方法执行了=");
    }

    public void showMethod(){
        print1(); // 子类的
    }
}

接下来写一个测试类,观察运行结果,我们发现都是调用的子类变量、子类方法。

public class Test {
    public static void main(String[] args) {
        // 目标:掌握子类中访问其他成员的特点:就近原则。
        Z z = new Z();
        z.showName();
        z.showMethod();
    }
}
  • 如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。
public class Z extends F {
    String name = "子类名称";

    public void showName(){
        String name = "局部名称";
        System.out.println(name); // 局部名称
        System.out.println(this.name); // 子类成员变量
        System.out.println(super.name); // 父类的成员变量
    }

    @Override
    public void print1(){
        System.out.println("==子类的print1方法执行了=");
    }

    public void showMethod(){
        print1(); // 子类的
        super.print1(); // 父类的
    }
}

2.7 子类中访问构造器的特点

我们先认识子类构造器的语法特点,再讲一下子类构造器的应用场景

子类中访问构造器的语法规则

  • 首先,子类全部构造器,都会先调用父类构造器,再执行自己。

    执行顺序,如下图按照① ② ③ 步骤执行:

1664160225526

子类访问构造器的应用场景

  • 如果不想使用默认的super()方式调用父类构造器,还可以手动使用super(参数)调用父类有参数构造器。

1664163881728

在本类中访问自己的构造方法

刚才我们学习了通过super()super(参数)可以访问父类的构造器。有时候我们也需要访问自己类的构造器。语法如下

this(): 调用本类的空参数构造器
this(参数): 调用本类有参数的构造器

1664170865036

最后我们被this和super的用法在总结一下

访问本类成员:
	this.成员变量	//访问本类成员变量
	this.成员方法	//调用本类成员方法
	this()		   //调用本类空参数构造器
    this(参数)	  //调用本类有参数构造器
	
访问父类成员:
	super.成员变量	//访问父类成员变量
	super.成员方法	//调用父类成员方法
	super()		   //调用父类空参数构造器
    super(参数)	  //调用父类有参数构造器
    
注意:thissuper访问构造方法,只能用到构造方法的第一句,否则会报错。

面向对象高级(二)

一、多态

1.1 多态概述

什么是多态?

多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。

比如:Teacher和Student都是People的子类,代码可以写成下面的样子

1664278943905

1664278943905

1.2 多态的好处

在多态形式下,右边的代码是解耦合的,更便于扩展和维护。

  • 怎么理解这句话呢?比如刚开始p1指向Student对象,run方法执行的就是Student对象的业务;假如p1指向Student对象 ,run方法执行的自然是Student对象的业务。

1665018279234

定义方法时,使用父类类型作为形参,可以接收一切子类对象,扩展更强,更便利。

public class Test2 {
    public static void main(String[] args) {
        // 目标:掌握使用多态的好处
		Teacher t = new Teacher();
		go(t);

        Student s = new Student();
        go(s);
    }

    //参数People p既可以接收Student对象,也能接收Teacher对象。
    public static void go(People p){
        System.out.println("开始------------------------");
        p.run();
        System.out.println("结束------------------------");
    }
}

1.3 类型转换

虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。

1665018661860

多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:

//如果p接收的是子类对象
if(父类变量 instance 子类){
    //则可以将p转换为子类类型
    子类 变量名 = (子类)父类变量;
}

1665018905475

如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型.

1665019335142

关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型

二、final关键字

2.1 final修饰符的特点

我们先来认识一下final的特点,final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。

- final修饰类:该类称为最终类,特点是不能被继承
- final修饰方法:该方法称之为最终方法,特点是不能被重写。
- final修饰变量:该变量只能被赋值一次。
  • 接下来我们分别演示一下,先看final修饰类的特点

1665020107661

  • 再来演示一下final修饰方法的特点

    1665020283101

  • 再演示一下final修饰变量的特点

    • 情况一

    1665020419364

    • 情况二

    1665020580223

    • 情况三

    1665020721501

    1665020951170

2.2 补充知识:常量

  • 被 static final 修饰的成员变量,称之为常量。
  • 通常用于记录系统的配置信息

接下来我们用代码来演示一下

public class Constant {
    //常量: 定义一个常量表示学校名称
    //为了方便在其他类中被访问所以一般还会加上public修饰符
    //常量命名规范:建议都采用大写字母命名,多个单词之前有_隔开
    public static final String SCHOOL_NAME = "传智教育";
}
public class FinalDemo2 {
    public static void main(String[] args) {
        //由于常量是static的所以,在使用时直接用类名就可以调用
        System.out.println(Constant.SCHOOL_NAME);
        System.out.println(Constant.SCHOOL_NAME);
        System.out.println(Constant.SCHOOL_NAME);
        System.out.println(Constant.SCHOOL_NAME);
        System.out.println(Constant.SCHOOL_NAME);
        System.out.println(Constant.SCHOOL_NAME);
        System.out.println(Constant.SCHOOL_NAME);
    }
}
  • 关于常量的原理,同学们也可以了解一下:在程序编译后,常量会“宏替换”,出现常量的地方,全都会被替换为其记住的字面量。把代码反编译后,其实代码是下面的样子
public class FinalDemo2 {
    public static void main(String[] args) {
        System.out.println("传智教育");
        System.out.println("传智教育"E);
        System.out.println("传智教育");
        System.out.println("传智教育");
        System.out.println("传智教育");
        System.out.println("传智教育");
        System.out.println("传智教育");
    }
}

三、抽象

3.1 认识抽象类

我们先来认识一下什么是抽象类,以及抽象类有什么特点。

  • 在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
-abstract修饰的类,就是抽象类
-abstract修饰的方法,就是抽象方法(不允许有方法体)

接下来用代码来演示一下抽象类和抽象方法

//abstract修饰类,这个类就是抽象类
public abstract class A{
    //abstract修饰方法,这个方法就是抽象方法
    public abstract void test();
}
  • 类的成员(成员变量、成员方法、构造器),类的成员都可以有。如下面代码
// 抽象类
public abstract class A {
    //成员变量
    private String name;
    static String schoolName;

    //构造方法
    public A(){

    }

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

    //实例方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • 抽象类是不能创建对象的,如果抽象类的对象就会报错

1665026273870

  • 抽象类虽然不能创建对象,但是它可以作为父类让子类继承。而且子类继承父类必须重写父类的所有抽象方法。
//B类继承A类,必须复写test方法
public class B extends A {
    @Override
    public void test() {

    }
}
  • 子类继承父类如果不复写父类的抽象方法,要想不出错,这个子类也必须是抽象类
//B类基础A类,此时B类也是抽象类,这个时候就可以不重写A类的抽象方法
public abstract class B extends A {

}

3.2 抽象类的好处

1665028790780

分析需求发现,该案例中猫和狗都有名字这个属性,也都有叫这个行为,所以我们可以将共性的内容抽取成一个父类,Animal类,但是由于猫和狗叫的声音不一样,于是我们在Animal类中将叫的行为写成抽象的。代码如下

public abstract class Animal {
    private String name;

    //动物叫的行为:不具体,是抽象的
    public abstract void cry();

    public String getName() {
        return name;
    }

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

接着写一个Animal的子类,Dog类。代码如下

public class Dog extends Animal{
    public void cry(){
        System.out.println(getName() + "汪汪汪的叫~~");
    }
}

然后,再写一个Animal的子类,Cat类。代码如下

public class Cat extends Animal{
    public void cry(){
        System.out.println(getName() + "喵喵喵的叫~~");
    }
}

最后,再写一个测试类,Test类。

public class Test2 {
    public static void main(String[] args) {
        // 目标:掌握抽象类的使用场景和好处.
        Animal a = new Dog();
        a.cry();	//这时执行的是Dog类的cry方法
    }
}

再学一招,假设现在系统有需要加一个Pig类,也有叫的行为,这时候也很容易原有功能扩展。只需要让Pig类继承Animal,复写cry方法就行。

public class Pig extends Animal{
    @Override
    public void cry() {
        System.out.println(getName() + "嚯嚯嚯~~~");
    }
}

此时,创建对象时,让Animal接收Pig,就可以执行Pig的cry方法

public class Test2 {
    public static void main(String[] args) {
        // 目标:掌握抽象类的使用场景和好处.
        Animal a = new Pig();
        a.cry();	//这时执行的是Pig类的cry方法
    }
}

综上所述,我们总结一下抽象类的使用场景和好处

1.用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,一提高代码的灵活性。

2.反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。

3.3 模板方法模式

那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题

比如A类和B类都有sing()方法,sing()方法的开头和结尾都是一样的,只是中间一段内容不一样。此时A类和B类的sing()方法中就存在一些相同的代码。

1665058597483

怎么解决上面的重复代码问题呢? 我们可以写一个抽象类C类,在C类中写一个doSing()的抽象方法。再写一个sing()方法,代码如下:

// 模板方法设计模式
public abstract class C {
    // 模板方法
    public final void sing(){
        System.out.println("唱一首你喜欢的歌:");

        doSing();

        System.out.println("唱完了!");
    }

    public abstract void doSing();
}

然后,写一个A类继承C类,复写doSing()方法,代码如下

public class A extends C{
    @Override
    public void doSing() {
        System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~");
    }
}

接着,再写一个B类继承C类,也复写doSing()方法,代码如下

public class B extends C{
    @Override
    public void doSing() {
        System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~");
    }
}

最后,再写一个测试类Test

public class Test {
    public static void main(String[] args) {
        // 目标:搞清楚模板方法设计模式能解决什么问题,以及怎么写。
        B b = new B();
        b.sing();
    }
}

综上所述:模板方法模式解决了多个子类中有相同代码的问题。具体实现步骤如下

1步:定义一个抽象类,把子类中相同的代码写成一个模板方法。
第2步:把模板方法中不能确定的代码写成抽象方法,并在模板方法中调用。
第3步:子类继承抽象类,只需要父类抽象方法就可以了。

四、接口

4.1 认识接口

我们先来认识一下接口?Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下

public interface 接口名{
    //成员变量(常量)
    //成员方法(抽象方法)
}

按照接口的格式,我们定义一个接口看看

public interface A{
    //这里public static final可以加,可以不加。
    public static final String SCHOOL_NAME = "黑马程序员";
    
    //这里的public abstract可以加,可以不加。
    public abstract void test();
}

写好A接口之后,在写一个测试类,用一下

public class Test{
    public static void main(String[] args){
        //打印A接口中的常量
        System.out.println(A.SCHOOL_NAME);
        
        //接口是不能创建对象的
        A a = new A();
    }
}

我们发现定义好接口之后,是不能创建对象的。那接口到底什么使用呢?需要我注意下面两点

  • 接口是用来被类实现(implements)的,我们称之为实现类。
  • 一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类

比如,再定义一个B接口,里面有两个方法testb1(),testb2()

public interface B {
    void testb1();
    void testb2();
}

接着,再定义一个C接口,里面有两个方法testc1(), testc2()

public interface C {
    void testc1();
    void testc2();
}

然后,再写一个实现类D,同时实现B接口和C接口,此时就需要复写四个方法,如下代码

// 实现类
public class D implements B, C{
    @Override
    public void testb1() {

    }

    @Override
    public void testb2() {

    }

    @Override
    public void testc1() {

    }

    @Override
    public void testc2() {

    }
}

最后,定义一个测试类Test

public class Test {
    public static void main(String[] args) {
        // 目标:认识接口。
        System.out.println(A.SCHOOL_NAME);

        // A a = new A();
        D d = new D();
    }
}

4.2 接口的好处

使用接口到底有什么好处呢?主要有下面的两点

  • 弥补了类单继承的不足,一个类同时可以实现多个接口。
  • 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。

我们看一个案例演示,假设有一个Studnet学生类,还有一个Driver司机的接口,还有一个Singer歌手的接口。

现在要写一个A类,想让他既是学生,偶然也是司机能够开车,偶尔也是歌手能够唱歌。那我们代码就可以这样设计,如下:

class Student{

}

interface Driver{
    void drive();
}

interface Singer{
    void sing();
}

//A类是Student的子类,同时也实现了Dirver接口和Singer接口
class A extends Student implements Driver, Singer{
    @Override
    public void drive() {

    }

    @Override
    public void sing() {

    }
}

public class Test {
    public static void main(String[] args) {
        //想唱歌的时候,A类对象就表现为Singer类型
        Singer s = new A();
        s.sing();
		
        //想开车的时候,A类对象就表现为Driver类型
        Driver d = new A();
        d.drive();
    }
}

综上所述:接口弥补了单继承的不足,同时可以轻松实现在多种业务场景之间的切换。

4.3 接口的案例

1665102202635

首先我们写一个学生类,用来描述学生的相关信息

public class Student {
    private String name;
    private char sex;
    private double score;

    public Student() {
    }

    public Student(String name, char sex, double score) {
        this.name = name;
        this.sex = sex;
        this.score = score;
    }

    public String getName() {
        return name;
    }

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

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }
}

接着,写一个StudentOperator接口,表示学生信息管理系统的两个功能。

public interface StudentOperator {
    void printAllInfo(ArrayList<Student> students);
    void printAverageScore(ArrayList<Student> students);
}

然后,写一个StudentOperator接口的实现类StudentOperatorImpl1,采用第1套方案对业务进行实现。

public class StudentOperatorImpl1 implements StudentOperator{
    @Override
    public void printAllInfo(ArrayList<Student> students) {
        System.out.println("----------全班全部学生信息如下--------------");
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore());
        }
        System.out.println("-----------------------------------------");
    }

    @Override
    public void printAverageScore(ArrayList<Student> students) {
        double allScore = 0.0;
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            allScore += s.getScore();
        }
        System.out.println("平均分:" + (allScore) / students.size());
    }
}

接着,再写一个StudentOperator接口的实现类StudentOperatorImpl2,采用第2套方案对业务进行实现。

public class StudentOperatorImpl2 implements StudentOperator{
    @Override
    public void printAllInfo(ArrayList<Student> students) {
        System.out.println("----------全班全部学生信息如下--------------");
        int count1 = 0;
        int count2 = 0;
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore());
            if(s.getSex() == '男'){
                count1++;
            }else {
                count2 ++;
            }
        }
        System.out.println("男生人数是:" + count1  + ", 女士人数是:" + count2);
        System.out.println("班级总人数是:" + students.size());
        System.out.println("-----------------------------------------");
    }

    @Override
    public void printAverageScore(ArrayList<Student> students) {
        double allScore = 0.0;
        double max = students.get(0).getScore();
        double min = students.get(0).getScore();
        for (int i = 0; i < students.size(); i++) {
            Student s = students.get(i);
            if(s.getScore() > max) max = s.getScore();
            if(s.getScore() < min) min = s.getScore();
            allScore += s.getScore();
        }
        System.out.println("学生的最高分是:" + max);
        System.out.println("学生的最低分是:" + min);
        System.out.println("平均分:" + (allScore - max - min) / (students.size() - 2));
    }
}

再写一个班级管理类ClassManager,在班级管理类中使用StudentOperator的实现类StudentOperatorImpl1对学生进行操作

public class ClassManager {
    private ArrayList<Student> students = new ArrayList<>();
    private StudentOperator studentOperator = new StudentOperatorImpl1();

    public ClassManager(){
        students.add(new Student("迪丽热巴", '女', 99));
        students.add(new Student("古力娜扎", '女', 100));
        students.add(new Student("马尔扎哈", '男', 80));
        students.add(new Student("卡尔扎巴", '男', 60));
    }

    // 打印全班全部学生的信息
    public void printInfo(){
        studentOperator.printAllInfo(students);
    }

    // 打印全班全部学生的平均分
    public void printScore(){
        studentOperator.printAverageScore(students);
    }
}

最后,再写一个测试类Test,在测试类中使用ClassMananger完成班级学生信息的管理。

public class Test {
    public static void main(String[] args) {
        // 目标:完成班级学生信息管理的案例。
        ClassManager clazz = new ClassManager();
        clazz.printInfo();
        clazz.printScore();
    }
}

注意:如果想切换班级管理系统的业务功能,随时可以将StudentOperatorImpl1切换为StudentOperatorImpl2。自己试试

4.4 接口JDK8的新特性

我们看一下这三种方法分别有什么特点?

public interface A {
    /**
     * 1、默认方法:必须使用default修饰,默认会被public修饰
     * 实例方法:对象的方法,必须使用实现类的对象来访问。
     */
    default void test1(){
        System.out.println("===默认方法==");
        test2();
    }

    /**
     * 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)
     *   实例方法:对象的方法。
     */
    private void test2(){
        System.out.println("===私有方法==");
    }

    /**
     * 3、静态方法:必须使用static修饰,默认会被public修饰
     */
     static void test3(){
        System.out.println("==静态方法==");
     }

     void test4();
     void test5();
     default void test6(){

     }
}

接下来我们写一个B类,实现A接口。B类作为A接口的实现类,只需要重写抽象方法就尅了,对于默认方法不需要子类重写。代码如下:

public class B implements A{
    @Override
    public void test4() {

    }

    @Override
    public void test5() {

    }
}

最后,写一个测试类,观察接口中的三种方法,是如何调用的

public class Test {
    public static void main(String[] args) {
        // 目标:掌握接口新增的三种方法形式
        B b = new B();
        b.test1();	//默认方法使用对象调用
        // b.test2();	//A接口中的私有方法,B类调用不了
        A.test3();	//静态方法,使用接口名调用
    }
}

综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。

4.5 接口的其他细节

注意事项:

  • 一个接口可以继承多个接口
public class Test {
    public static void main(String[] args) {
        // 目标:理解接口的多继承。
    }
}

interface A{
    void test1();
}
interface B{
    void test2();
}
interface C{}

//比如:D接口继承C、B、A
interface D extends C, B, A{

}

//E类在实现D接口时,必须重写D接口、以及其父类中的所有抽象方法。
class E implements D{
    @Override
    public void test1() {

    }

    @Override
    public void test2() {

    }
}

接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)

1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。

综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。

面向对象高级(三)

一、内部类

内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。

当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。

比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。

public class Car{
	//内部类
    public class Engine{
        
    }
}

内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。

1.1 成员内部类

成员内部类就是类中的一个普通成员,类似于成员变量、成员方法。

public class Outer {
    private int age = 99;
    public static String a="黑马";

    // 成员内部类
    public class Inner{
        private String name;
        private  int age = 88;

        //在内部类中既可以访问自己类的成员,也可以访问外部类的成员
        public void test(){
            System.out.println(age); //88
            System.out.println(a);   //黑马

            int age = 77;
            System.out.println(age); //77
            System.out.println(this.age); //88
            System.out.println(Outer.this.age); //99
        }

        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;
        }
    }
}

成员内部类如何创建对象,格式如下

//外部类.内部类 变量名 = new 外部类().new 内部类();
Outer.Inner in = new Outer().new Inner();
//调用内部类的方法
in.test();

总结一下内部类访问成员的特点

  • 既可以访问内部类成员、也可以访问外部类成员
  • 如果内部类成员和外部类成员同名,可以使用**类名.this.成员**区分

1.2 静态内部类

静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。

public class Outer {
    private int age = 99;
    public static String schoolName="黑马";

    // 静态内部类
    public static class Inner{
        //静态内部类访问外部类的静态变量,是可以的;
        //静态内部类访问外部类的实例变量,是不行的
        public void test(){
            System.out.println(schoolName); //99
            //System.out.println(age);   //报错
        }
    }
}

静态内部类创建对象时,需要使用外部类的类名调用。

//格式:外部类.内部类 变量名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
in.test();

1.3 局部内部类

局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。

public class Outer{
    public void test(){
        //局部内部类
        class Inner{
            public void show(){
                System.out.println("Inner...show");
            }
        }
        
        //局部内部类只能在方法中创建对象,并使用
        Inner in = new Inner();
        in.show();
    }
}

1.4 匿名内部类*

1.4.1 认识匿名内部类,基本使用

一种在实际开发中用得最多的一种内部类,叫匿名内部类。相比于前面几种内部类,匿名内部类就比较重要的。

我们还是先认识一下什么是匿名内部类?

匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。

下面就是匿名内部类的格式:

new 父类/接口(参数值){
    @Override
    重写父类/接口的方法;
}

匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。

比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。

public abstract class Animal{
    public abstract void cry();
}

接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类

public class Test{
    public static void main(String[] args){
        //这里后面new 的部分,其实就是一个Animal的子类对象
        //这里隐含的有多态的特性: Animal a = Animal子类对象;
        Animal a = new Animal(){
            @Override
            public void cry(){
                System.out.println("猫喵喵喵的叫~~~");
            }
        }
        a.eat(); //直线上面重写的cry()方法
    }
}

需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以外部类$1.class的方法命名

1665658585267

匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。

1.4.2 匿名内部类的应用场景

一般我们会主动的使用匿名内部类。

**只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。**这样就可以少写一个类。比如,看下面代码

public interface Swimming{
    public void swim();
}
public class Test{
    public static void main(String[] args){
        Swimming s1 = new Swimming(){
            public void swim(){
                System.out.println("狗刨飞快");
            }
        };
        go(s1);
        
        Swimming s1 = new Swimming(){
            public void swim(){
                System.out.println("猴子游泳也还行");
            }
        };
        go(s1);
    }
    //形参是Swimming接口,实参可以接收任意Swimming接口的实现类对象
    public static void go(Swimming s){
        System.out.println("开始~~~~~~~~");
        s.swim();
        System.out.println("结束~~~~~~~~");
    }
}

二、枚举

2.1 认识枚举

2.1.1 认识枚举、枚举的原理

枚举是一种特殊的类,它的格式是:

public enum 枚举类名{
    枚举项1,枚举项2,枚举项3;
}

其实枚举项就表示枚举类的对象,只是这些对象在定义枚举类时就预先写好了,以后就只能用这几个固定的对象。

我们用代码演示一下,定义一个枚举类A,在枚举类中定义三个枚举项X, Y, Z

public enum A{
    X,Y,Z;
}

想要获取枚举类中的枚举项,只需要用类名调用就可以了

public class Test{
    public static void main(String[] args){
        //获取枚举A类的,枚举项
        A a1 = A.X;
        A a2 = A.Y;
        A a3 = A.Z;
    }
}

刚才说,枚举项实际上是枚举类的对象,这一点其实可以通过反编译的形式来验证(需要用到反编译的命令,这里不能直接将字节码拖进idea反编译)

1665669996020

我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final 修饰,所以被可以类名调用,而且不能更改。

2.1.2 枚举深入

既然枚举是一个类的话,我们能不能在枚举类中定义构造器、成员变量、成员方法呢?答案是可以的。来看一下代码吧

public enum A{
    //定义枚举项
    X,Y,Z("张三"); //枚举项后面加括号,就是在执行枚举类的带参数构造方法。
    
    //定义空构造器
    public A(){
        
    }
    
    //成员变量
    private String name;
    //定义带参数构造器
    public A(String name){
        this.name=name;
    }
    
    //成员方法
    public String getName(){
        return name;
    }
    ...
}

虽然枚举类中可以像类一样,写一些类的其他成员,但是一般不会这么写,如果你真要这么干的话,到不如直接写普通类来的直接。

2.2 枚举的应用场景

枚举的应用场景是这样的:枚举一般表示一组信息,然后作为参数进行传输。

我们来看一个案例。比如我们现在有这么一个应用,用户进入应用时,需要让用户选择是女生、还是男生,然后系统会根据用户选择的是男生,还是女生推荐不同的信息给用户观看。

1665670887179

这里我们就可以先定义一个枚举类,用来表示男生、或者女生

public class Constant{
    BOY,GRIL
}

再定义一个测试类,完成用户进入系统后的选择

public class Test{
    public static void main(String[] args){
        //调用方法,传递男生
        provideInfo(Constant.BOY);
    }
    
    public static void provideInfo(Constant c){
        switch(c){
            case BOY:
                System.out.println("展示一些信息给男生看");
                break;
            case GRIL:
                System.out.println("展示一些信息给女生看");
                break;
        }
    }
}

最终再总结一下枚举的应用场景:枚举一般表示几个固定的值,然后作为参数进行传输

三、泛型

3.1 认识泛型

所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:),称为泛型类、泛型接口、泛型方法、它们统称为泛型。

ArrayList类就是一个泛型类,打开API文档看一下ArrayList类的声明。

1665671616852

ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>表示元素的数据类型。

当别人使用ArrayList集合创建对象时,new ArrayList<String> 就表示元素为String类型,new ArrayList<Integer>表示元素为Integer类型。

1665671987771

我们总结一下泛型的作用、本质:

  • 泛型的好处:在编译阶段可以避免出现一些非法的数据。

  • 泛型的本质:把具体的数据类型传递给类型变量。

3.2 自定义泛型类

泛型类,在实际工作中一般都是源代码中写好,我们直接用的,就是ArrayList这样的,自己定义泛型类是非常少的

自定义泛型类的格式如下

//这里的<T,W>其实指的就是类型变量,可以是一个,也可以是多个。
public class 类名<T,W>{
    
}

接下来,我们自己定义一个MyArrayList泛型类,模拟一下自定义泛型类的使用。注意这里重点仅仅只是模拟泛型类的使用,所以方法中的一些逻辑是次要的,也不会写得太严谨。

//定义一个泛型类,用来表示一个容器
//容器中存储的数据,它的类型用<E>先代替用着,等调用者来确认<E>的具体类型。
public class MyArrayList<E>{
    private Object[] array = new Object[10];
    //定一个索引,方便对数组进行操作
    private int index;
    
    //添加元素
    public void add(E e){
        array[index]=e;
        index++;
    }
    
    //获取元素
    public E get(int index){
        return (E)array[index];
    }
}

接下来,我们写一个测试类,来测试自定义的泛型类MyArrayList是否能够正常使用

public class Test{
    public static void main(String[] args){
        //1.确定MyArrayList集合中,元素类型为String类型
        MyArrayList<String> list = new MyArrayList<>();
        //此时添加元素时,只能添加String类型
        list.add("张三");
        list.add("李四");
        
         //2.确定MyArrayList集合中,元素类型为Integer类型
        MyArrayList<Integer> list1 = new MyArrayList<>();
        //此时添加元素时,只能添加String类型
        list.add(100);
        list.add(200);
        
    }
}

3.3 自定义泛型接口

泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>表示。定义格式如下:

//这里的类型变量,一般是一个字母,比如<E>
public interface 接口名<类型变量>{
    
}

比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。

首先我们得有一个学生类和老师类

public class Teacher{

}
public class Student{
    
}

我们定义一个Data<T>泛型接口,T表示接口中要处理数据的类型。

public interface Data<T>{
    public void add(T t);
    
    public ArrayList<T> getByName(String name);
}

接下来,我们写一个处理Teacher对象的接口实现类

//此时确定Data<E>中的E为Teacher类型,
//接口中add和getByName方法上的T也都会变成Teacher类型
public class TeacherData implements Data<Teacher>{
   	public void add(Teacher t){
        
    }
    
    public ArrayList<Teacher> getByName(String name){
        
    }
}

接下来,我们写一个处理Student对象的接口实现类

//此时确定Data<E>中的E为Student类型,
//接口中add和getByName方法上的T也都会变成Student类型
public class StudentData implements Data<Student>{
   	public void add(Student t){
        
    }
    
    public ArrayList<Student> getByName(String name){
        
    }
}

3.4 泛型方法

public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){
    
}

下图中在返回值类型和修饰符之间有定义的才是泛型方法。

1665750638693

接下我们看一个泛型方法的案例

public class Test{
    public static void main(String[] args){
        //调用test方法,传递字符串数据,那么test方法的泛型就是String类型
        String rs = test("test");
    
        //调用test方法,传递Dog对象,那么test方法的泛型就是Dog类型
    	Dog d = test(new Dog()); 
    }
    
    //这是一个泛型方法<T>表示一个不确定的数据类型,由调用者确定
    public static <T> test(T t){
        return t;
    }
}

3.5 泛型限定

学习一个泛型的特殊用法,叫做泛型限定。泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式

  • <?> 表示任意类型
  • <? extends 数据类型> 表示指定类型或者指定类型的子类
  • <? super 数据类型> 表示指定类型或者指定类型的父类

下面我们演示一下,假设有Car作为父类,BENZ,BWM两个类作为Car的子类,代码如下

class Car{}
class BENZ extends Car{}
class BWN extends Car{}

public class Test{
    public static void main(String[] args){
        //1.集合中的元素不管是什么类型,test1方法都能接收
        ArrayList<BWM> list1 = new ArrayList<>();
        ArrayList<Benz> list2 = new ArrayList<>();
        ArrayList<String> list3 = new ArrayList<>();
        test1(list1);
        test1(list2);
        test1(list3);
        
        //2.集合中的元素只能是Car或者Car的子类类型,才能被test2方法接收
        ArrayList<Car> list4 = new ArrayList<>();
        ArrayList<BWM> list5 = new ArrayList<>();
        test2(list4);
        test2(list5);
        
        //2.集合中的元素只能是Car或者Car的父类类型,才能被test3方法接收
        ArrayList<Car> list6 = new ArrayList<>();
        ArrayList<Object> list7 = new ArrayList<>();
        test3(list6);
        test3(list7);
    }
    
    public static void test1(ArrayList<?> list){
        
    }
    
    public static void test2(ArrayList<? extends Car> list){
        
    }
    
   	public static void test3(ArrayList<? super Car> list){
        
    }
}

3.6 泛型擦除

泛型擦除。什么意思呢?也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。

把下面的代码的字节码进行反编译

1665752105271

下面是反编译之后的代码,我们发现ArrayList后面没有泛型

1665752037764

四、常用API

API(Application Programming interface)意思是应用程序编程接口,说人话就是Java帮我们写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。

1665752705389

我们要学习那些API呢?

1665752813753

“千里之行始于足下,多记、多查、多些代码、孰能生巧!”

4.1 Object类

Object类。Object类是Java中所有类的祖宗类,因此,Java中所有类的对象都可以直接使用Object类中提供的一些方法。

按照下图的提示,可以搜索到你想要找的类

1665753230409

我们找到Object类的下面两个方法

1665753282718

  • 4.1.1 toString()方法

我们先来学习toString()方法。

public String toString()
    调用toString()方法可以返回对象的字符串表示形式。
    默认的格式是:“包名.类名@哈希值16进制”

假设有一个学生类如下

public class Student{
    private String name;
    private int age;
    
    public Student(String name, int age){
        this.name=name;
        this.age=age;
    }
}

再定义一个测试类

public class Test{
    public static void main(String[] args){
        Student s1 = new Student("赵敏",23);
        System.out.println(s1.toString()); 
    }
}

打印结果如下

1665753662732

如果,在Student类重写toString()方法,那么我们可以返回对象的属性值,代码如下

public class Student{
    private String name;
    private int age;
    
    public Student(String name, int age){
        this.name=name;
        this.age=age;
    }
    
    @Override
    public String toString(){
        return "Student{name=‘"+name+"’, age="+age+"}";
    }
}

运行测试类,结果如下

1665754067446

4.1.2 equals(Object o)方法

接下来,我们学习一下Object类的equals方法

public boolean equals(Object o)
    判断此对象与参数对象是否"相等"

我们写一个测试类,测试一下

public class Test{
	public static void main(String[] args){
        Student s1 = new Student("赵薇",23);
        Student s2 = new Student("赵薇",23);
        
        //equals本身也是比较对象的地址,和"=="没有区别
        System.out.println(s1.equals(s2)); //false
         //"=="比较对象的地址
        System.out.println(s1==s2); //false
    }
}

但是如果我们在Student类中,把equals方法重写了,就按照对象的属性值进行比较

public class Student{
    private String name;
    private int age;
    
    public Student(String name, int age){
        this.name=name;
        this.age=age;
    }
    
    @Override
    public String toString(){
        return "Student{name=‘"+name+"’, age="+age+"}";
    }
    
    //重写equals方法,按照对象的属性值进行比较
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        if (age != student.age) return false;
        return name != null ? name.equals(student.name) : student.name == null;
    }
}

再运行测试类,效果如下

1665754859931

总结一下Object的toString方法和equals方法

public String toString()
   	返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16进制”
   	【子类重写后,返回对象的属性值】
   	
public boolean equals(Object o)
    判断此对象与参数对象是否"相等"。默认比较对象的地址值,和"=="没有区别
    【子类重写后,比较对象的属性值】

4.1.3 clone() 方法

接下来,我们学习Object类的clone()方法,克隆。意思就是某一个对象调用这个方法,这个方法会复制一个一模一样的新对象,并返回。

public Object clone()
    克隆当前对象,返回一个新对象

想要调用clone()方法,必须让被克隆的类实现Cloneable接口。如我们准备克隆User类的对象,代码如下

public class User implements Cloneable{
    private String id; //编号
    private String username; //用户名
    private String password; //密码
    private double[] scores; //分数

    public User() {
    }

    public User(String id, String username, String password, double[] scores) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.scores = scores;
    }

    //...get和set...方法自己加上

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

接着,我们写一个测试类,克隆User类的对象。并观察打印的结果

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        User u1 = new User(1,"zhangsan","wo666",new double[]{99.0,99.5});
		//调用方法克隆得到一个新对象
        User u2 = (User) u1.clone();
        System.out.println(u2.getId());
        System.out.println(u2.getUsername());
        System.out.println(u2.getPassword());
        System.out.println(u2.getScores()); 
    }
}

我们发现,克隆得到的对象u2它的属性值和原来u1对象的属性值是一样的。

1665757008178

上面演示的克隆方式,是一种浅克隆的方法,浅克隆的意思:拷贝出来的对象封装的数据与原对象封装的数据一模一样(引用类型拷贝的是地址值)。如下图所示

1665757187877

还有一种拷贝方式,称之为深拷贝,拷贝原理如下图所示

1665757265609

下面演示一下深拷贝User对象

public class User implements Cloneable{
    private String id; //编号
    private String username; //用户名
    private String password; //密码
    private double[] scores; //分数

    public User() {
    }

    public User(String id, String username, String password, double[] scores) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.scores = scores;
    }

    //...get和set...方法自己加上

	@Override
    protected Object clone() throws CloneNotSupportedException {
        //先克隆得到一个新对象
        User u = (User) super.clone();
        //再将新对象中的引用类型数据,再次克隆
        u.scores = u.scores.clone();
        return u;
    }
}

1665757536274

4.2 Objects类

Objects是一个工具类,提供了一些方法可以对任意对象进行操作。主要方法如下

1665760840329

下面写代码演示一下这几个方法

public class Test{
    public static void main(String[] args){
        String s1 = null;
        String s2 = "itheima";
        
        //这里会出现NullPointerException异常,调用者不能为null
        System.out.println(s1.equals(s2));
        //此时不会有NullPointerException异常,底层会自动先判断空
        System.out.println(Objects.equals(s1,s2));
        
        //判断对象是否为null,等价于==
        System.out.println(Objects.isNull(s1)); //true
        System.out.println(s1==null); //true
        
        //判断对象是否不为null,等价于!=
        System.out.println(Objects.nonNull(s2)); //true
        System.out.println(s2!=null); //true
    }
}

4.3 基本类型包装类

为什么要学习包装类呢?因为在Java中有一句很经典的话,万物皆对象。Java中的8种基本数据类型还不是对象,所以要把它们变成对象,变成对象之后,可以提供一些方法对数据进行操作。

Java中8种基本数据类型都用一个包装类与之对一个,如下图所示

1665758797003

学习包装类两要点:

    1. 创建包装类的对象方式、自动装箱和拆箱的特性;
    1. 利用包装类提供的方法对字符串和基本类型数据进行相互转换

4.2.1 创建包装类对象

我们先来学习,创建包装类对象的方法,以及包装类的一个特性叫自动装箱和自动拆箱。我们以Integer为例,其他的都是类似的。

//1.创建Integer对象,封装基本类型数据10
Integer a = new Integer(10);

//2.使用Integer类的静态方法valueOf(数据)
Integer b = Integer.valueOf(10);

//3.还有一种自动装箱的写法(意思就是自动将基本类型转换为引用类型)
Integer c = 10;

//4.有装箱肯定还有拆箱(意思就是自动将引用类型转换为基本类型)
int d = c;

//5.装箱和拆箱在使用集合时就有体现
ArrayList<Integer> list = new ArrayList<>();
//添加的元素是基本类型,实际上会自动装箱为Integer类型
list.add(100);
//获取元素时,会将Integer类型自动拆箱为int类型
int e = list.get(0);

4.2.2 包装类数据类型转换

在开发中,经常使用包装类对字符串和基本类型数据进行相互转换。

  • 把字符串转换为数值型数据:包装类.parseXxx(字符串)
public static int parseInt(String s)
    把字符串转换为基本数据类型
  • 将数值型数据转换为字符串:包装类.valueOf(数据);
public static String valueOf(int a)
    把基本类型数据转换为
  • 写一个测试类演示一下
//1.字符串转换为数值型数据
String ageStr = "29";
int age1 = Integer.parseInt(ageStr);

String scoreStr = 3.14;
double score = Double.prarseDouble(scoreStr);

//2.整数转换为字符串,以下几种方式都可以(挑中你喜欢的记一下)
Integer a = 23;
String s1 = Integer.toString(a);
String s2 = a.toString();
String s3 = a+"";
String s4 = String.valueOf(a);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值