【JAVA基础】由C++快速上手Java(超详细)

前言:笔者是普通本科大三学生。本科学的语言是C++,但是想找后端开发相关的工作,因此开始学习java。笔者是通过学习黑马程序员的课程入门java的。这个基础课程涉及到的很多内容在学习C++的过程都有涉及,在这里做一个由C++基础开始学习java的快速入门经验总结。学习视频链接:【黑马程序员Java+AI智能辅助编程全套视频教程,java零基础入门到大牛一套通关】

目录

一、Java概述

1、JDK

“一次编译,处处运行”

2、Java是一门纯粹的面向对象的语言。

2.1 mian()方法写在类中

2.2 保存类信息的Class类

3、Java的数据类型

4、数据的在内存中的存储

一个栗子

二、C++到Java快速上手语法

1、Java的项目结构

2、注释

3、运算符

>>>

Java不支持运算符重载

4、分支结构

switch

try-catch-finall

5、数组

一维数组

二维数组

        锯齿数组

三、Java的面对对象

构造器

this关键字

static关键字

静态块

继承

语法:

权限修饰符

方法重写

重写toString()

子类构造器调用父类构造器

多态

概述

多态实现的底层逻辑

多态下的类型转换

其他

lombok库(为类自动生成getter方法


持续更新ing……

2025.3.6 第一次更新 更新static关键字以及后面的内容

2025.3.13 第二次更新 更新多态以及后面的内容


一、Java概述

        这些笔者认为是快速上手java的核心知识,是java和c++的核心区别。有了这些知识能很快理解java的各种设计。

1、JDK

使用java首先要下载的就是jdk(Java Development Kit),用于构建和运行Java应用程序的完整开发环境。jdk里面包括:

  • JVM:虚拟机,java程序实际上的运行环境,也是java“一次编译,处处运行”的关键。
  • 核心类库:Java官方提供的标准库,包含了大量预定义的类和接口,供开发者调用。
  • 开发工具javac(将Java源程序.java编译成字节码文件.class)、java(Java解释器,用于在JVM里运行编译后的Java程序。)

“一次编译,处处运行”

        Java的一个核心特性,也是Java使用广泛的原因。由于java程序在JVM上运行,所以java程序可以在任何安装了有合适的JVM的平台上运行。

2、Java是一门纯粹的面向对象的语言。

2.1 mian()方法写在类中

        与C++不同的是,c++程序的入口main函数可以作为全局的函数,不写在类中的,但是Java中所有函数(java里面称为“方法”)包括main方法都是写在一个类中的。使用intellJ IDEA运行某个类的时候,实际就是运行这个类里面定义的main方法;如果这个类没有定义main方法,则不能将这个类作为程序的入口点,这个类只能被其他代码调用。

2.2 保存类信息的Class类

        对于用户自定义的各种类和java官方提供的各种类,这些类的信息都是保存在一个Class类的实例对象中的。类的信息包括但不限于 类名、成员变量、构造器(即C++中的构造函数)、成员方法……

        也就是说,java官方提供了一个类,类名就是Class。程序执行的时候,对于用户自定义的每一个类,JVM将会Class实例,将这些类的信息保存到这个Class的实例中。

        类似的,对于类里面的构造器,java官方也提供了一个Constructor类,对于每个构造器,会使用一个Constructor实例来保存这个构造器的信息(如这个构造器是否私有)。

3、Java的数据类型

java中所有类型分为两大类:

  • 基本数据类型(类型后面的数字代表该类型几字节)
    • 整型(byte 1、short  2、int  4、long  8)、
    • 浮点型(float  4、double  8)
    • 字符型(char  2)
    • 布尔型(boolean :true / false)

注意:java中的char是无符号的;布尔类型和整型之间不能互相显示/隐式转换。

关于boolean的大小,Java规范并没有明确规定它占用多少字节,通常认为它是1位(bit),但实际上在内存中可能是1字节或更多,具体取决于JVM实现。

  • 引用数据类型
    • 其余任何类型。包括但不限于类、字符串String(String的本质也是一个类)、数组
    • 当一个变量存储的是一个引用类型数据的时候,该变量保存的实则是该数据在内存中的“地址”而非其本身。
    • 所有引用类型都是Object类的后代。

        注意:Java没有指针的概念

4、数据的在内存中的存储

每个运行的java程序的内存分为三大块:

  • (局部变量)
  • (引用数据类型存储的位置:eg类的对象实例、数组)
  • 方法区(已被虚拟机加载的类信息、常量、静态变量) 
一个栗子
public class A {
    int value;
    void fun() {...}
    static void sfun() {...}
}

public class Main {
    public static void main(String[] args) {
        A a = new A(); // step 1:创建A的实例a
        a.fun();       // step 2:调用实例方法fun()
        A.sfun();      // step 3:调用静态方法sfun()
    }
}
  • 当Main.main方法开始执行时,会在栈中创建一个新的栈帧,用于存储局部变量a。a是一个引用类型的变量,它存储的是指向堆中A实例的引用(即对象的内存地址)。
  • new A() 创建了一个A的实例,并将其存储在堆中 。这个实例包含了成员变量value、A类在方法区中的位置。
  • 类A的信息(包括类名、成员变量、构造器、成员方法等)存储在方法区中。

调用a.fun()的过程

  1. 首先在栈中找到局部变量a,获取其值(即指向堆中A实例的引用)。
  2. 根据a的值找到堆中对应的A实例。
  3. 在堆中找到A实例对应的类信息(位于方法区),从而找到fun()方法的相关信息。
  4. fun()方法的相关信息加载到栈上,并执行该方法。

调用a.sfun()的过程

  1. 直接在方法区中找到类A的静态方法sfun()
  2. 加载sfun()方法的相关信息到栈上,并执行该方法。

二、C++到Java快速上手语法

1、Java的项目结构

初学者主要了解以下项目结构。初学者写的Hello,World就是新建一个class文件写的。

  • project
    • model(一般开发一个单独的功能,比如微信的朋友圈模块)
      • package(用于组织类文件,对于不同包,在后面类的访问性修饰符有影响)
        • class(如果一个类文件里面只有一个类,这个文件名需要和类名保持完全一致,包括大小写)

2、注释

        相同点:  // 单行注释     /* 多行注释 */

        不同点:文档注释

(笔者认为对于初学者来说不是重点,不会影响后续学习。笔者对此的了解也甚是浅薄)

        Java 提供了一种专门用于生成文档的注释格式。有了这种注释格式,可以自动为编写的代码生成文档,这些文档通常以html文件的形式呈现。非常适合一些大型项目或者多人合作项目,方便查阅类和方法的作用。

        文档注释以 /** 开始,并以 */ 结束。与普通注释不同,文档注释通常放置在类、接口、方法和字段声明之前。为了更好地描述代码,文档注释中可以使用一系列预定义的标签(tags),如“@author”。

下面我将以一个栗子简单说明文档注释的用法:

假如我有以下文件Example.java。

//有“树树:”代表这是笔者写的文档注释,并没有实际含义

/**
 * 树树:这是一个简单的类,用于演示文档注释。
 *
 * @author 张三
 * @version 1.0
 * @since 1.0
 */
public class Example {
    /**
     * 树树:一个简单的实例变量。
     */
    private int value;

    /**
     * 树树:构造函数,初始化实例变量。
     *
     * @param initialValue 初始值
     * @throws IllegalArgumentException 如果初始值小于0
     */
    public Example(int initialValue) throws IllegalArgumentException {
        if (initialValue < 0) {
            throw new IllegalArgumentException("Initial value cannot be negative");
        }
        this.value = initialValue;
    }

    /**
     * 树树:获取实例变量的值。
     *
     * @return 实例变量的值
     * @see #setValue(int)
     */
    public int getValue() {
        return value;
    }

    /**
     * 树树:设置实例变量的值。
     *
     * @param newValue 新值
     * @deprecated 使用 {@link #setValue(int)} 代替
     */
    @Deprecated
    public void setValue(int newValue) {
        this.value = newValue;
    }

    /**
     * 树树:设置实例变量的新值。
     *
     * @param newValue 新值
     */
    public void setValueNew(int newValue) {
        this.value = newValue;
    }
}

        使用命令为这个文件的文档注释生成文档(实则命令可以更复杂,为文档指定生成位置等,具体方法可以ai一下(。◕‿◕。)

javadoc Example.java

        然后就能在对应文档生成位置找到文档。其实生成的文档是静态html网页资源

        打开allclasses-index.html可以看见所有类的信息;点开index-all.html可以看见所有文档注释的内容。网页上方还提供了一个搜索框以便快速定位信息。

3、运算符

大部分Java运算符的用法和功能都和C++一致,这里只说不同点。

>>>

C++没有该运算符。在 Java 中,>>> 是一个无符号右移运算符。它用于将一个整数的所有位向右移动指定的位数,并且在左侧填充0,无论该整数的最高位是0还是1。

public class Main {
    public static void main(String[] args) {
        int signed = -8; // 11111111 11111111 11111111 11111000
        int unsignedRightShift = signed >>> 2; // 00111111 11111111 11111111 11111110
        System.out.println(unsignedRightShift); // 输出 1073741822

        int signedRightShift = signed >> 2; // 11111111 11111111 11111111 11111110
        System.out.println(signedRightShift); // 输出 -2
    }
}

Java不支持运算符重载

4、分支结构

switch

1、Java支持的数据类型:byte、short、int、char、枚举(JDK5开始)、String(JDK7开始)

2、case的值只能是字面量或者是编译时常量,而非表达式

3、增强switch(JDK12开始),允许一个case匹配多个标签;引入符号“->”,使用该符号,每个case后面的代码块不用“break”也不会fall-through。

int dayOfWeek = 2;
switch (dayOfWeek) {
    case 1, 2, 3 -> System.out.println("Weekday");
    case 6, 7 -> System.out.println("Weekend");
    default -> System.out.println("Invalid day");
}

try-catch-finall

        和c++相比,java多了一个finally块。不论try块里面的程序有没有成功执行,finally块里面的代码都最总会执行。(当然不要finally块,只要try-catch也是可以的。)

try {
    // 可能抛出异常的代码
} catch (Exception e) {
    // 处理异常
} finally {
    // 总是执行的代码
}

5、数组

一维数组

        数组声明:如果只是声明数组变量,但是没有初始化,那此时这个数组变量的初始值是null,因为它没有指向任何对象。此时在内存中并没有为这个数组分配空间。

int[] intArray; // 声明一个整数数组,此时intArray为null

       数组初始化:必须指定数组内每个元素的值或者显示分配空间(new)。

int[] arr={1,2,3};//初始化方式一:直接指定数组内每个元素的值
int[] arr=new int[5]; //初始化方式二:先不指定数组内每个元素的值,但是分配了内存

        边界检查:Java中如果越界访问元素会抛出异常

        length属性:数组本质上也是一个类,继承自Object。初始化一个数组就是创建了一个数组实例;声明一个数组就是创建一个“指向在堆中的数组实例”的引用变量。因此每个数组对象将会有Java提供的方法和属性。其中最常用的是length属性,用于获取数组的长度。

int[] intArray = {1, 2, 3, 4, 5};
System.out.println("Length of intArray: " + intArray.length); // 输出: 5

二维数组

       普通二维数组的声明、使用和C++类似。

//二维数组声明
int[][] arrayName1; //方法一
int arrayName2[][]; //方法二

//二维数组初始化
//静态初始化
int[][] arrayName3 = {
    {1, 2, ..., n},
    {1, 2, ..., n},
    // ...
};
//动态初始化
int[][] arrayName4 = new int[3][4];

//使用
int[0][0]=1;
        锯齿数组

        Java中的二维数组本质是数组的数组,而每个数组都是一个引用对象。也就是说,二维数组本质上是一个一维数组,只不过这个数组里面的每个元素都是一个一维数组的地址(即一维数组的引用对象)。这意味着,和C++不同的是,Java中的二维数组不必每一行的元素都一样多。

栗子

//方法一:
int[][] jaggedArray = new int[3][]; // 只指定行数
jaggedArray[0] = new int[]{1, 2, 3}; // 第一行有3个元素
jaggedArray[1] = new int[]{4, 5}; // 第二行有2个元素
jaggedArray[2] = new int[]{6, 7, 8, 9}; // 第三行有4个元素

System.out.println("Length of first row: " + jaggedArray[0].length); // 输出: 3
System.out.println("Length of second row: " + jaggedArray[1].length); // 输出: 2
System.out.println("Length of third row: " + jaggedArray[2].length); // 输出: 4

//方法二:
int[][] jaggedArray2 = {
    {1, 2},
    {1, 2, 3,4}
};

三、Java的面对对象

构造器

        和C++一样,Java中每个类都有一个构造器(C++里称为构造函数)用于创建该类的实例。如果在自定义的类里面没有声明构造器,Java编译器将会自动生成一个无参构造器;如果有声明其他构造器(不论是有参还是无参),Java编译器将不会自动生成编译器。这意味着如果你同时需要无参构造器和有参构造器,你需要手动实现这两种构造器。

        C++有但是Java不支持的:

  • 复制构造函数   在进行引用变量之间的相互赋值的时候,并不会创建新的实例,它们只是进行引用赋值
  • 析构函数   Java 没有析构函数的概念。Java 使用垃圾回收机制自动管理内存,当对象不再被引用时,垃圾回收器会在适当的时候回收其占用的内存。
  • 初始化列表  
this关键字

        C++中的this是一个指针,Java中this是一个引用变量,指向对象本身,它们的作用相似。可以使用this访问当前对象的属性或者方法,或者通过this()访问当前对象的构造器。

栗子

public class Rectangle {
    private double width;
    private double height;

    //使用this访问属性
    public Rectangle(double width, double height) {
        this.width = width;    
        this.height = height;
    }

    // 使用this()访问构造器:
    public Rectangle() {
        this(1.0, 1.0); 
    }
}

构造函数委托

上面那个栗子中,在一个构造器中调用同一个类的另一个构造器,被称为构造函数委托。其中要注意的是this()调用其他构造器的代码需要放在第一行

static关键字

修饰成员变量:与C++类似。该变量属于类而不是对象,所有对象共享同一个变量。可以通过类名或者对象名来访问该变量。

修饰方法:与C++类似。该方法属于类而不是对象,所有对象共享。可以通过类名或者对象名来访问该方法。但是不能在静态方法中访问非静态成员。静态方法不能被override(重写),只能被隐藏

tip:工具类中的方法常常被声明为静态的,以便于使用工具类提供的功能。工具类的构造器通常被设计为私有的,这意味着理论上无法创建对应实例,因为工具类通常只是提供一个功能,创建实例对于工具类来说通常是不必要的。如Java官方提供的Math工具类提供各种数学运算方法:

double result = Math.pow(2, 3); // 8.0

静态成员变量/方法 VS 非静态的成员变量/方法:访问前者只需要类名即可访问,而后者需要创建实例才能访问。

静态代码块

Java中的静态初始化块是C++中没有的,它在类加载时(注意不是创建对象时)执行,且只会执行一次。静态块通常用于复杂的静态变量初始化。

public class MyClass {
    static int counter;

    static { // 静态初始化块
        counter = 5;
        System.out.println("Static block executed");
    }
}

类的五大成分

  • 成员变量、构造器、方法、代码块、内部类

        前三成分C++中也有我们较为熟悉,再次只着重说明后两部分。

代码块

代码块分为静态代码块实例代码块。静态代码块就是上文提到的,会在类加载的时候执行一次钱。

实例代码块 不含static的代码块,包括在 {} 中,在创建对象的时候执行一次,且先于构造器执行。

public class MyClass {
    //实例代码块
    {
        System.out.println("Instance block executed");
    }

    public MyClass() {
        System.out.println("Constructor executed");
    }

    public static void main(String[] args) {
        new MyClass(); // 输出: Instance block executed, Constructor executed
        new MyClass(); // 输出: Instance block executed, Constructor executed (每次创建对象时都会执行实例代码块)
    }
}

内部类

定义在类内部的类。分为四种:成员内部类静态内部类局部内部类匿名内部类

成员内部类

定义在外部类内的非静态类,属于外部类的对象所持有。它可以直接访问外部类的所有成员(包括私有成员),并且可以通过外部类的对象来创建实例。

public class OuterClass {
    private int outerField = 10;

    // 成员内部类
    public class InnerClass {
        public void display() {
            System.out.println("Outer field value: " + outerField); // 可以直接访问外部类的成员
        }
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        OuterClass.InnerClass inner = outer.new InnerClass(); // 需要通过外部类对象创建内部类实例
        inner.display(); // 输出: Outer field value: 10
    }
}
静态内部类

也叫静态嵌套类,是使用 static 关键字修饰的内部类。它可以看作是外部类的一个静态成员,因此不能直接访问外部类的非静态成员(但可以访问静态成员);但可以通过类名来创建实例

public class OuterClass {
    private static int staticOuterField = 20;
    private int nonStaticOuterField = 30;

    // 静态嵌套类
    public static class StaticNestedClass {
        public void display() {
            System.out.println("Static outer field value: " + staticOuterField); // 可以访问静态成员
            // System.out.println(nonStaticOuterField); // 错误:无法访问非静态成员
        }
    }

    public static void main(String[] args) {
        // 创建静态嵌套类的实例
        OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
        nested.display(); // 输出: Static outer field value: 20
    }
}
局部内部类

定义在方法或代码块中的类。它只能在该方法或代码块的作用域内使用,并且可以访问外部类的所有成员以及方法中的局部变量(前提是这些局部变量必须是 final 或者实际上是不可变的)。

public class OuterClass {
    private int outerField = 40;

    public void someMethod() {
        final int localVar = 50; // 局部变量必须是 final 或者实际上是不可变的

        // 局部内部类
        class LocalInnerClass {
            public void display() {
                System.out.println("Outer field value: " + outerField);
                System.out.println("Local variable value: " + localVar);
            }
        }

        LocalInnerClass localInner = new LocalInnerClass();
        localInner.display(); // 输出: Outer field value: 40, Local variable value: 50
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.someMethod();
    }
}
匿名内部类

匿名内部类是没有名字的内部类,通常用于实现接口或扩展抽象类。它们在定义时立即实例化,并且通常只使用一次。

// 定义一个接口
interface Printable {
    void print();
}

public class OuterClass {

    public static void main(String[] args) {
        // 使用匿名内部类实现 Printable 接口
        Printable printable = new Printable() {
            @Override
            public void print() {
                System.out.println("Printing from anonymous inner class");
            }
        };

        printable.print(); // 输出: Printing from anonymous inner class
    }
}

继承

语法

  • 通过extends关键字实现继承
  • java中的继承都是公有的
  • 不支持多重继承
//继承
public class Father{
    private int value;
    public void fun(){...}
}

public class Son extends Father{
    //...
}
  • 通过关键字abstract来定义抽象类、抽象方法:和C++一样,Java中的类中如果有抽象方法(类似于C++的虚函数),该类就是抽象类。就算类中没有抽象方法,也可以使用abstract关键字将类声明为抽象类。抽象类不能被实例化,通常需要一个子类继承该抽象类并实现了所有抽象方法,该子类才能被实例化。
public abstract class Animal {
    // 抽象方法
    public abstract void makeSound();

    // 普通方法
    public void sleep() {
        System.out.println("The animal is sleeping.");
    }
}

权限修饰符

修饰符同一包内的类子孙类(包括其他包)所有类说明
private
default默认情况下没有显式的修饰符。
protected
public

        Java中需要为每一个成员变量或者方法显示声明权限修饰符(除非使用默认权限default)

方法重写

  • @override 注解:写在方法的上一行,告诉编译器这是一个重写方法,编译器在编译阶段将会检查父类中是否有同名、同参的方法。
  • 子类重写父类方法时,重写的方法的访问权限应该≥父类对应方法;该方法的返回值的类型应该≤父类对应方法返回值。(返回值类型≤意思是该类型应该是父类方法对应返回值的类型或者其子孙类型)
  • 私有方法、静态方法不能被重写。
重写toString()

使用语句System.out.print(data)可以将变量输出到工作台上。对于基本数据类型和String,直接将变量打印到工作台上。对于引用数据类型,默认输出为类名@哈希码。其底层是调用Object的toString(),输出的是toString()方法的返回值。如果想自定义类的输出,应该重写类的toString()。

栗子

public class Person {
    private String name;
    private int age;

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

    ...
}

子类构造器调用父类构造器

  • 子类构造器在执行之前会先调用父类构造器。因此如果父类只有一个构造器,且这个构造器私有的话会报错,通常认为这个父类并不能被继承
  • 子类构造器在默认情况下会在代码第一行自动调用super()。但是如果父类没有无参构造器,子类构造器内的第一行要手动super(参数列表)
class Parent {
    public Parent(int value) {
       // ...
    }
}

class Child extends Parent {
    public Child() {
        super(10); // 必须显式调用父类的有参构造器
        //...
    }
}

多态

概述

java中的多态强调对象多态、行为多态,但不强调成员变量多态。java中常常使用多态,即声明一个父类的引用类型数据,但是其指向的具体实现是子类。

多态的好处:

  • 右边的真实对象和左边声明的引用类型是解耦合的。
  • 定义父类类型的形参,可以接受一切其子孙类型作为实参。使得代码的拓展性更强

class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

class Cat : public Animal {
public:
    void makeSound() const override {
        std::cout << "Cat meows" << std::endl;
    }
};


public class Main {
    public static void main(String[] args) {
        //在这个例子中,myAnimal 是 Animal 类型的引用,但它实际上指向的是 Dog 类型的对象。
        //即使 Dog 类的具体实现发生变化,只要 makeSound() 方法的签名保持不变,
        //main 方法中的代码不需要修改。
        Animal myAnimal = new Dog(); // 父类引用指向子类对象
        myAnimal.makeSound(); // 输出: Dog barks
    
        //在这个例子中,makeAnimalSound 函数接受一个 Animal* 类型的参数,
        //它可以接受任何 Animal 的派生类对象。
        makeAnimalSound(new Dog()); // 输出: Dog barks
        makeAnimalSound(new Cat()); // 输出: Cat meows
    }
}

注意:多态情况下声明的引用类型,不能调用对应子类实例独有的功能,只能调用声明类型有的功能。原因涉及多态的底层实现逻辑

多态实现的底层逻辑

方法表:JVM加载类的时候,为每一个类创建一个方法表,该表记录了该类每个方法的入口。子类会继承父类的方法表,如果子类Override了父类的某个方法,其方法表上对应的方法的入口也会重载。

编译时类型检查:java源码在编译的时候会进行类型检查。可以认为,实现多态的时候,编译器只认一个引用变量声明时的类型而不在乎其具体引用对象的类型。它会检查这个引用变量调用的方法是否属于父类的方法,如果调用子类的独有方法编译器会认为类型不匹配而报错。

动态绑定:程序运行的时候,在调用一个引用类型的方法的时候,实际上查找的时实例对应的类型的方法表。以此实现多态。

多态下的类型转换

向上转换(自动)  向上转型是安全的,因为子类对象一定是父类类型。

Animal myAnimal = new Dog(); // 向上转型,Dog 对象被当作 Animal 类型

向下转换(手动 / 显示转换) 只有引用类型指向的对象确实是转换之后的类型才能转换,否则会抛出类型转换异常

Animal myAnimal = new Dog();
Dog myDog = (Dog) myAnimal; // 向下转型,将 Animal 引用转换为 Dog 类型
//如果myAnimal指向的类型是Cat会在运行时抛出类型转换的异常。

final关键字

该关键字可以理解为最后一次被定义。

  • 修饰类,该类不能被继承(工具类可以考虑使用final修饰)
  • 修饰方法,该方法不能被重写
  • 修饰变量,该变量仅能在声明时被赋值一次。
    • 如果修饰的是引用类型变量,该变量指向别的实例对象,但是指向的实例对象本身的内容时可以改变的。(类似C++的指针,指针指向的位置不变,但是指针指向位置上的内容是可以改变的)
常量static final
  • Java中称被static final修饰的变量为常量
  • 常量的标识符一般使用大写加下划线。(eg.  MAX_VALUE)
  • Java中,编译时能确定值的常量将会在.class文件中被替换字面量(即内联)。
  • 因此将常用的配置信息定义成一个常量包,即便于管理又不会影响性能。

抽象(abstract关键字)

  • 被abstract修饰的类是抽象类,不能被实例化,只有其非抽象的子孙类能实例化;
  • 被abstract修饰的方法是抽象方法,无需具体实现。
  • 抽象类中不一定有抽象方法;有抽象方法的类一定是抽象类。
// 定义一个抽象类
abstract class Animal {
    // 抽象方法,没有方法体
    abstract void makeSound();
    // 普通方法
    void sleep() {
        System.out.println("This animal is sleeping.");
    }
}
// 定义一个子类,继承抽象类
class Dog extends Animal {
    // 实现抽象方法
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}
// 测试类
public class Main {
    public static void main(String[] args) {
        // 创建子类对象
        Dog myDog = new Dog();
        // 调用实现的方法
        myDog.makeSound(); // 输出: Woof! Woof!
        // 调用继承的普通方法
        myDog.sleep(); // 输出: This animal is sleeping.
        // 以下代码会报错,因为抽象类不能被实例化
        // Animal myAnimal = new Animal();
    }
}

*接口(interface)

  • 接口可以被类继承,且一个类可以继承多个接口,这弥补了Java中类与类之间不能多继承的缺陷。
  • 接口不能被实例化
  • 如果一个类继承了某个接口并且实现了该接口里定义的所有抽象方法,则称这个类为该接口的实现类;否则该类需要被设计成抽象类
JDK8前的传统接口
  • 只能有常量成员变量抽象方法
public interface Animal {
    // 抽象方法:方法默认为公有抽象的;即自动加上关键字public abstract
    void makeSound();

    // 常量字段:成员变量默认为公有常量,且只能是常量
    //即自动加上关键字 public final static
    String KINGDOM = "Animalia";
}
JDK8后的接口

JDK8后的接口(新增三种方法)

  • 默认方法(需要加关键字 default):即普通方法,可以被实现类继承、重写。但是不能直接通过接口名调用;只能通过实现类或者其对象调用。
  • 私有方法(需要加关键字private):只能由接口中的其他实例方法(有方法体的方法)调用。
  • 静态方法(需要关键字static):默认为public的,属于接口的方法,可以直接通过接口名调用。实现类可以通过隐藏(Hide)的方式重载方法(类似C++的重载,需要参数列表不同)
public interface Animal {
    // 抽象方法:
    void makeSound();

    // 默认方法:带有默认实现的方法,可以被实现类继承或重写
    default void sleep() {
        System.out.println("Animal is sleeping");
    }

    // 私有方法:只能在接口内部调用,用于辅助其他方法
    private void performAction(String action) {
        System.out.println("Performing: " + action);
    }

    // 另一个默认方法,使用私有方法
    default void eat() {
        performAction("eating");
    }

    // 静态方法:属于接口本身,可以通过接口名直接调用
    static void displayKingdom() {
        System.out.println("Kingdom: Animalia");
    }
}
接口的使用

继承接口的类称为该接口的实现类(使用关键字 implement),实现类必须要实现该接口的所有抽象方法;否则需要将该类定义为抽象类

实现类

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }

    // 重写默认方法
    @Override
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

一个类实现多个接口,需要实现该接口定义的所有抽象方法(除非把这个类定义为抽象类)

public interface Swimmable {
    void swim();
}

public interface Flyable {
    void fly();
}

public class Duck implements Swimmable, Flyable {
    @Override
    public void swim() {
        System.out.println("Duck is swimming");
    }

    @Override
    public void fly() {
        System.out.println("Duck is flying");
    }
}

使用接口的实现类

public class Main {
    public static void main(String[] args) {
        // 创建 Dog 对象并通过 Animal 接口引用
        Animal myDog = new Dog();
        myDog.makeSound(); // 输出: Dog barks
        myDog.sleep();     // 输出: Dog is sleeping (重写了默认方法)
        myDog.eat();       // 输出: Performing: eating (使用了私有方法)

        // 创建 Cat 对象并通过 Animal 接口引用
        Animal myCat = new Cat();
        myCat.makeSound(); // 输出: Cat meows
        myCat.sleep();     // 输出: Animal is sleeping (使用默认方法)
        myCat.eat();       // 输出: Performing: eating (使用了私有方法)

        // 调用静态方法
        Animal.displayKingdom(); // 输出: Kingdom: Animalia
    }
}

该例子中使用Animal的引用类型来承接该接口的实现类实例,这种写法在Java中常用,尤其在使用Java提供的API时经常使用。

使用接口的好处
  • 多态性:
    • eg. myDog 和 myCat 都是 Animal 类型的引用,但它们分别指向了 Dog 和 Cat 的实例。在一个方法中接受 Animal 类型的参数,而无需关心传入的具体实现类是什么。
public void animalSound(Animal animal) {
    animal.makeSound();
}

// 调用
animalSound(new Dog()); // 输出: Dog barks
animalSound(new Cat()); // 输出: Cat meows
  • 解耦合
  • 增强可扩展性
  • 强制实现规范
  • 提高代码复用性

其他注意事项

  • 一个类继承了父类和接口,如果父类和接口中有同名方法(包括参数列表相同),优先使用父类。
  • 如果一个类继承了多个接口,这些接口中存在冲突的同名默认方法,需要重写该方法。

四、注解

这部分内容涉及反射机制。也就是说,注解的底层实现借助保存类信息的特殊的class类(前文提到)

关于反射机制这篇文章没有提到,因为反射和注解都属于java高级的内容。初学者暂时跳过并无大碍。但是注解是java中十分重要的内容,专业的java开发人员必须掌握这部分内容。

java中的特殊标记,如常见注解@Test、@Override。其作用是让其他程序根据注解信息来决定如何执行该程序。

注解可以用在类上、构造器上、方法上、成员变量上、参数上等位置。

自定义注解

注意:属性名后面要带括号!因为虽然它被称为属性,但在实现上,注解是官方接口Annotation的实现类。每次使用接口的时候都会创建该注解的类的一个实例。

public @interface 注解名{
    public 属性类型 属性名() default 默认值;
}

自定义注解的使用方法

下面通过一个例子来说明注解的使用方法:

假如定义了以下注解:

//MyBook.java
public @interface MyBook {
    String name();
    int age() default 18;
    String[] address();
}

则该注解的使用为

//BookDemo.java
@MyBook(name = "小王子",address = {"B612星球", "地球"})
public class BookDemo {
    //...
}

属性age有默认值,因此使用该注解的时候可以不给age赋值。

特殊属性value

在 Java 注解里,value 是一个特殊的预定义属性名。如果注解里仅有一个value属性,那么在使用注解的时候可以不标明属性名。

【例子】

假如有以下注解

public @interface Test {
    String value(); // 唯一属性
}

在使用该注解的时候可以省略属性名

@Test("测试用例") // 最简洁的语法
public void myTestMethod() {}

如果某个注解里除了value属性之外,其他属性独有默认值,在使用该注解的时候也可以使用简略语法。

【例子】

public @interface MyAnnotation {
    String value(); // 无默认值,必须显式赋值
    int id() default 0; // 有默认值
    String[] tags() default {}; // 有默认值
}

// 正确使用:
@MyAnnotation("简化语法") // 等价于 @MyAnnotation(value = "简化语法")
public class MyClass {}

// 也可以显式指定其他属性:
@MyAnnotation(value = "完整语法", id = 123)
public class MyClass2 {}

元注解

用来注解注解的注解。官方提供了5中内置的元注解。以下两种是常用的元注解

  • @Target:声明被修饰的注解只能在哪些地方使用
  • @Retention:声明注解的保留周期

【例子】

下面是@Target的使用

//MyAnnotation1.java
@Target(ElementType.METHOD) // 限制注解仅能用于方法
public @interface MyAnnotation1 {
    // 注解定义
}

//MyAnnotation2.java
//@Target注解接受的是一个列表,可以选择多个枚举类型
@Target({ElementType.METHOD,ElementType.PARAMETER}) // 限制注解仅能用于方法和参数
public @interface MyAnnotation2 {
    // 注解定义
}

注解的解析

简单来说注解的解析就是分析一个类,或者类里面的一个方法,或者……上面有没有标注注解,并把注解里的信息拿出来分析的过程。既然要分析类的信息,势必要借助反射机制来实现。

下面用一个例子说明注解的解析

假设有如下自定义注解和自定义的类

//MyBook.java
//假如有以下自定义注解
public @interface MyBook {
    String name();
    int age() default 18;
    String[] address();
}

//BookInfo.java
// 使用注解
@MyBook(name = "小王子", address = {"B612星球", "地球"})
class BookInfo {
    // ...
}

解析注解

 ps:不了解反射机制的话,只需要简单理解成每一个类可以通过Class类(一个Java官方定义的类)获取这个类Class类的实例保存的就是自定义类的类信息。每一个自定义方法都可以通过一个Method类来获取这个方法的信息,每一个Method类的实例保存的就是这个自定义方法的信息。

// AnnotationParser.java
class AnnotationParser {
    public static void main(String[] args) {
        //通过class类获取BookInfo类的信息
        Class<BookInfo> bookInfoClass = BookInfo.class;
        //如果BookInfo类被MyBook注解
        if (bookInfoClass.isAnnotationPresent(MyBook.class)) {
            //将注解内容取出,做后续操作
            MyBook myBookAnnotation = bookInfoClass.getAnnotation(MyBook.class);
            System.out.println("书名: " + myBookAnnotation.name());
            System.out.println("年龄: " + myBookAnnotation.age());
            System.out.print("地址: ");
            for (String address : myBookAnnotation.address()) {
                System.out.print(address + " ");
            }
        }
    }
}

其他

lombok库(为类自动生成getter方法)

这个库的主要功能是提供了一些很有用的注解

  • @Getter / @Setter:自动生成字段的 Getter 和 Setter 方法。

  • @ToString:自动生成 toString() 方法。

  • @EqualsAndHashCode:自动生成 equals() 和 hashCode() 方法。

  • @Data:组合注解,包含 @Getter@Setter@ToString@EqualsAndHashCode 等功能。

  • @NoArgsConstructor:生成无参构造函数。

  • @AllArgsConstructor:生成全参构造函数。

  • @RequiredArgsConstructor:生成包含 final 字段或 @NonNull 字段的构造函数。

Getter / Setter 方法

在实际的开发项目中,通常需要为类中的每个成员变量提供共有的get方法(用于获取该成员变量的值)和公有的set方法(用于修改该成员变量的值),然后将这些成员变量设为私有的。这么做是为了

  • 隐藏实现细节:类的内部实现可以随时更改,而不会影响使用该类的代码。

  • 控制访问权限:通过 Getter 和 Setter 方法,可以控制字段的读写权限。

  • 数据验证:在 Setter 方法中可以添加逻辑,确保数据的有效性。

虽然 Getter 和 Setter 方法很重要,但手动编写它们会增加代码量。Lombok 库通过注解自动生成这些方法,从而简化代码。

栗子

源文件:使用@Getter  @Setter注解

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class User {
    private String name;
    private int age;
}

编译成的.class文件:会自动生成每个成员变量的get方法和set方法

public class User {
    private String name;
    private int age;

    // Getter for name
    public String getName() {
        return this.name;
    }
    // Setter for name
    public void setName(String name) {
        this.name = name;
    }
    // Getter for age
    public int getAge() {
        return this.age;
    }
    // Setter for age
    public void setAge(int age) {
        this.age = age;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值