这是我读大学时的Java知识点总结,还不全面,后续会逐渐增加完善。

知识点集合

实例变量

实例变量是指在类中声明的变量,其值是针对类的每个实例而独立存储的。每个类的实例都有自己的一组实例变量,它们的值可以在对象创建时初始化,并在整个对象的生命周期中保持不变或者随着对象的状态而改变。

实例变量也被称为对象变量,因为它们是在类的对象实例化时创建的,并且每个对象都有自己的一组实例变量。

实例变量的特点包括:

  • 它们属于对象,而不是类本身。
  • 每个对象都有自己的一组实例变量,每个对象的实例变量值可以是不同的。
  • 它们在对象的整个生命周期中存在,并且可以被对象的方法访问和修改。
  • 实例变量不能使用 static 关键字进行修饰,而是通过实例化对象来访问。

以下是一个示例,演示了如何在Java中定义和使用实例变量:

public class Person {
    // 实例变量
    String name;
    int age;
    
    public void displayInfo() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
    }
}

// 创建对象并设置实例变量的值
Person person = new Person();
person.name = "John";
person.age = 25;

// 调用方法访问和使用实例变量
person.displayInfo();

在这个例子中,Person 类有两个实例变量 nameage,它们属于每个 Person 对象的一部分。在创建 Person 对象后,可以通过访问对象的实例变量来设置和获取它们的值。在 displayInfo() 方法中,可以使用实例变量来展示对象的信息。

因此,实例变量是定义在类中的变量,每个对象都有自己独立的一组实例变量,用于存储对象的状态和属性。

实例化对象

实例化对象是根据类的定义创建一个具体的对象,也可以说是将类实例化为对象的过程。

在面向对象编程中,类是对象的模板,描述了对象应该具有的属性和行为。通过实例化对象,我们可以根据类的定义创建一个具体的实体,该实体具有类中定义的属性和行为。

在Java中,通过使用 new 关键字和类的构造方法来实例化对象。构造方法是一种特殊的方法,用于创建对象并初始化其属性。

以下是一个示例:

// 定义一个类
class Person {
    private String name;
    private int age;
    
    // 构造方法
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 方法
    public void sayHello() {
        System.out.println("Hello, my name is " + name + " and I'm " + age + " years old.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 实例化对象
        Person person = new Person("John", 25);
        
        // 调用对象的方法
        person.sayHello(); // 输出 "Hello, my name is John and I'm 25 years old."
    }
}

在上面的示例中,我们定义了一个 Person 类,具有 nameage 属性,以及 sayHello() 方法。通过 new 关键字和 Person 类的构造方法,我们实例化了一个 Person 对象,并将其赋值给 person 变量。

通过 person 对象,我们可以调用 sayHello() 方法,该方法会输出对象的属性值。

实例化对象是面向对象编程的基本概念之一,它允许我们创建具体的对象实例,并根据类定义的属性和行为进行操作。

抽象类

Java 中的抽象类是不能被实例化的,只能被继承。抽象类是用来作为其他类的父类或基类,它本身不能被实例化为对象。

抽象类是通过在类定义中使用关键字 abstract 来标识的。抽象类可以包含抽象方法,这些方法没有具体的实现,需要在子类中进行重写实现。抽象类可以有普通方法和成员变量,可以提供一些共享的实现逻辑给子类使用。

以下是一个示例:

// 抽象类
abstract class Animal {
    // 抽象方法
    public abstract void makeSound();
    
    // 普通方法
    public void sleep() {
        System.out.println("Animal is sleeping");
    }
}

// 继承抽象类
class Cat extends Animal {
    // 实现抽象方法
    public void makeSound() {
        System.out.println("Meow");
    }
}

public class Main {
    public static void main(String[] args) {
        // 错误示例,不能实例化抽象类
        // Animal animal = new Animal();
        
        // 创建子类对象
        Animal cat = new Cat();
        cat.makeSound(); // 输出 "Meow"
        cat.sleep(); // 输出 "Animal is sleeping"
    }
}

在上面的示例中,Animal 类是一个抽象类,其中包含了一个抽象方法 makeSound() 和一个普通方法 sleep()。不能直接实例化 Animal 类,但可以通过继承它的子类 Cat 来创建对象。

多态性

Java 中的多态性(polymorphism)是指在一个类层次结构中,子类可以以自己的形式重写父类的方法,并且可以使用父类的引用来引用子类的对象。这种特性允许我们通过父类的引用来调用子类特定的方法,从而实现不同类型的对象以相同的方式进行操作。

多态性是面向对象编程的重要特性之一,它提高了代码的灵活性和可扩展性。通过多态性,我们可以编写通用的代码,而不需要针对每个具体的子类编写独立的代码。

以下是一个示例:

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

// 子类1
class Dog extends Animal {
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

// 子类2
class Cat extends Animal {
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog(); // 子类对象赋值给父类引用
        Animal animal2 = new Cat(); // 子类对象赋值给父类引用
        
        animal1.makeSound(); // 调用子类的方法,输出 "Dog barks"
        animal2.makeSound(); // 调用子类的方法,输出 "Cat meows"
    }
}

在上面的示例中,我们定义了一个 Animal 父类和两个子类 DogCat。通过将子类对象赋值给父类引用,我们可以使用父类的引用来调用子类的方法。在 main 方法中,我们创建了一个 Dog 对象,并将其赋值给 Animal 类型的引用 animal1,以及创建了一个 Cat 对象,并将其赋值给 Animal 类型的引用 animal2。然后,我们通过这两个引用调用了 makeSound() 方法,分别输出了相应的子类特定的声音。

子类不能覆盖父类的私有(private)方法

在 Java 中,子类不能覆盖父类的私有(private)方法。私有方法是指只能在声明它的类内部访问的方法,无法被其他类或子类所访问。

子类继承父类的方法有以下几种情况:

  1. 如果父类的方法是公共(public)或受保护(protected)的,子类可以重写(覆盖)该方法。
  2. 如果父类的方法是默认访问修饰符(即没有修饰符)的,子类可以重写该方法,前提是子类与父类在同一个包中。
  3. 如果父类的方法是私有的,子类无法访问该方法,因此也无法重写它。

以下是一个示例:

class Parent {
    public void publicMethod() {
        System.out.println("Parent's public method");
    }
    
    protected void protectedMethod() {
        System.out.println("Parent's protected method");
    }
    
    private void privateMethod() {
        System.out.println("Parent's private method");
    }
}

class Child extends Parent {
    // 重写父类的公共方法
    public void publicMethod() {
        System.out.println("Child's public method");
    }
    
    // 重写父类的受保护方法
    protected void protectedMethod() {
        System.out.println("Child's protected method");
    }
    
    // 无法重写父类的私有方法,编译报错
    // private void privateMethod() {
    //     System.out.println("Child's private method");
    // }
}

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        parent.publicMethod(); // 输出 "Parent's public method"
        parent.protectedMethod(); // 输出 "Parent's protected method"
        // parent.privateMethod(); // 编译报错,私有方法无法访问
        
        Child child = new Child();
        child.publicMethod(); // 输出 "Child's public method"
        child.protectedMethod(); // 输出 "Child's protected method"
    }
}

在上面的示例中,我们定义了一个 Parent 父类和一个 Child 子类。父类中有三个方法,分别是公共方法、受保护方法和私有方法。子类继承了父类,并重写了父类的公共方法和受保护方法。然而,子类无法重写父类的私有方法,因为私有方法只能在父类内部访问。

Runnable 接口并实现 run() 方法

Runnable 是一个接口,其中定义了一个抽象方法 run(),该方法包含线程的执行逻辑。实现了 Runnable 接口的类需要实现 run() 方法,并在该方法中编写线程的具体执行逻辑。

创建线程的步骤如下:

  1. 创建一个类,实现 Runnable 接口。例如:
public class MyRunnable implements Runnable {
    public void run() {
        // 线程的执行逻辑
        System.out.println("Thread is running.");
    }
}
  1. 在实现了 Runnable 接口的类中,编写线程的具体执行逻辑,即在 run() 方法中定义线程的行为。
  2. 在主线程或其他线程中,创建 Thread 对象,并将实现了 Runnable 接口的类的实例作为参数传递给 Thread 的构造函数。例如:
public class Main {
    public static void main(String[] args) {
        // 创建实现了 Runnable 接口的类的实例
        MyRunnable myRunnable = new MyRunnable();

        // 创建 Thread 对象,并将实现了 Runnable 接口的类的实例作为参数传递
        Thread thread = new Thread(myRunnable);

        // 启动线程
        thread.start();
    }
}

通过将实现了 Runnable 接口的类的实例传递给 Thread 的构造函数,可以创建一个新的线程,并在该线程中执行实现了 run() 方法的代码逻辑。

这种方式的优势是可以实现多重继承,因为 Java 不支持多重继承,但可以实现多个接口。同时,它也更加灵活,因为同一个 Runnable 对象可以被多个线程共享,从而实现线程的资源共享。

总之,通过实现 Runnable 接口并实现 run() 方法,可以创建一个新的类作为线程的执行体,并在创建线程时将该类的实例传递给 Thread 对象来创建新线程。

先进后出(LIFO)

先进后出(Last-In-First-Out,LIFO)是一种数据结构,其中最后插入的元素首先被访问或删除。以下是实现先进后出的一些常见数据结构:

  1. 堆栈(Stack):堆栈是一种基于 LIFO 原则的数据结构,它使用 push() 方法将元素添加到栈的顶部,并使用 pop() 方法从栈的顶部删除和访问元素。
  2. 递归调用:在编程中,递归调用也可以看作是一种先进后出的行为。每次进行递归调用时,当前的函数调用被暂停并推入调用栈,直到递归结束开始逐个弹出并执行。

需要注意的是,先进后出是与先进先出(FIFO)相对的概念。在先进后出的数据结构中,最后插入的元素首先被访问或删除;而在先进先出的数据结构中,最先插入的元素首先被访问或删除。

先进先出(FIFO)

JAVA异常

在 Java 中,异常分为两种类型:编译时异常(Checked Exception)和运行时异常(Runtime Exception)。
编译时异常(Checked Exception)是在编译阶段检测到的异常,需要在代码中进行处理或声明抛出。它们通常表示程序在运行过程中可能出现的外部因素引起的错误或异常情况。IOException 是编译时异常的一个常见例子,表示输入输出操作可能出现的错误,例如文件不存在或读写错误等。
运行时异常(Runtime Exception)是在程序运行时发生的异常,不需要在代码中进行强制处理或声明抛出。它们通常表示程序逻辑错误或编程错误,例如除以零、数组越界等。RuntimeException 是运行时异常的一个常见例子。

静态方法

静态方法是在类级别上定义的方法,与特定的对象实例无关。它属于类本身,而不是类的实例。可以通过类名直接调用静态方法,而无需创建类的对象。

public class MyClass {
    private static int count; // 静态变量

    public static void staticMethod() {
        System.out.println("这是一个静态方法");
    }

    public void instanceMethod() {
        System.out.println("这是一个实例方法");
    }

    public static int getCount() {
        return count;
    }

    public static void setCount(int value) {
        count = value;
    }
}

在上面的代码中,我们定义了一个名为MyClass的类。其中包含一个静态方法staticMethod()和一个实例方法instanceMethod()。还有一个静态变量count,并提供了静态的getter和setter方法。

可以通过以下方式调用静态方法和访问静态变量:

MyClass.staticMethod(); // 调用静态方法

MyClass myObject = new MyClass();
myObject.instanceMethod(); // 调用实例方法

int currentCount = MyClass.getCount(); // 获取静态变量的值
MyClass.setCount(10); // 设置静态变量的值

请注意,静态方法可以直接通过类名调用,而实例方法需要通过类的对象调用。静态方法可以在没有类的实例的情况下使用,因为它们属于类本身。而实例方法需要通过类的对象来调用,因为它们与特定的对象实例相关联。

静态变量

public class StaticVariableExample {
    // 静态变量
    static int staticVariable = 10;
    // 实例变量
    int instanceVariable = 20;

    public static void main(String[] args) {
        // 直接通过类名访问静态变量
        System.out.println("静态变量的值:" + StaticVariableExample.staticVariable);

        // 创建类的实例对象
        StaticVariableExample obj = new StaticVariableExample();
        // 通过实例对象访问实例变量
        System.out.println("实例变量的值:" + obj.instanceVariable);

        // 修改静态变量的值
        StaticVariableExample.staticVariable = 30;
        // 修改实例变量的值
        obj.instanceVariable = 40;

        // 再次访问静态变量和实例变量的值
        System.out.println("修改后的静态变量的值:" + StaticVariableExample.staticVariable);
        System.out.println("修改后的实例变量的值:" + obj.instanceVariable);
    }
}
//输出结果:
//静态变量的值:10
//实例变量的值:20
//修改后的静态变量的值:30
//修改后的实例变量的值:40

成员变量

成员变量(Member Variables)是定义在类中的变量,也称为实例变量或对象属性。它们是类的组成部分,用于存储对象的状态和数据。

成员变量在类的内部声明,但在方法之外。它们可以有不同的访问修饰符(如public、private、protected等),用于控制其可见性和访问权限。

每个类的实例(对象)都有自己的一组成员变量,它们独立于其他对象的成员变量。当创建一个类的实例时,会为该实例分配一块内存来存储其成员变量的值。

成员变量可以是任何合法的Java数据类型,包括基本数据类型(如int、double、boolean等)和引用数据类型(如String、数组等)。每个对象的成员变量都有默认的初始值,例如,数值类型的默认值是0,布尔类型的默认值是false,引用类型的默认值是null。

通过使用对象引用和点操作符(.)可以访问和修改对象的成员变量。每个对象都有自己独立的一组成员变量,可以通过对象引用来访问和操作属于该对象的成员变量。

总结来说,成员变量是定义在类中的变量,用于存储对象的状态和数据。每个对象都有自己独立的一组成员变量,通过对象引用和点操作符可以访问和操作这些成员变量。

当我们定义一个类时,可以在类的内部声明成员变量。下面是一个示例代码,解释了成员变量的用法:

public class MyClass {
    // 成员变量
    private int myNumber; // 整数类型的成员变量
    private String myString; // 字符串类型的成员变量

    // 构造方法,用于创建对象时初始化成员变量
    public MyClass(int number, String str) {
        myNumber = number;
        myString = str;
    }

    // 成员方法,用于访问成员变量和进行操作
    public void printDetails() {
        System.out.println("Number: " + myNumber);
        System.out.println("String: " + myString);
    }

    // 主方法,用于执行程序
    public static void main(String[] args) {
        // 创建一个对象并传入初始化参数
        MyClass obj = new MyClass(10, "Hello");
        // 调用成员方法来访问和操作成员变量
        obj.printDetails();
    }
}

在上述代码中,我们定义了一个名为MyClass的类,并声明了两个成员变量myNumbermyString。在构造方法中,我们初始化这两个成员变量。然后,在成员方法printDetails()中,我们访问并打印了这两个成员变量的值。

在主方法中,我们创建了一个MyClass对象obj,并传入初始化参数10和"Hello"。然后,我们调用obj的成员方法printDetails(),它会输出成员变量myNumbermyString的值。

通过这个例子,我们可以看到成员变量的用法。每个对象都有自己独立的一组成员变量,我们可以通过对象引用来访问和操作这些成员变量。

ArrayList和LinkedList

ArrayList和LinkedList是Java中两种常见的集合类,它们都实现了List接口,但在内部实现和使用方式上有一些区别。

内部实现方式:
ArrayList:基于数组实现,可以利用索引进行快速访问和修改元素。插入和删除元素时需要移动其他元素的位置,效率较低。
LinkedList:基于双向链表实现,每个元素都包含前后指针,插入和删除元素时只需要修改相邻元素的指针,效率较高。

访问效率:
ArrayList:由于基于数组实现,可以通过索引进行快速访问和修改元素。时间复杂度为O(1)。
LinkedList:由于基于链表实现,访问和修改元素需要遍历链表,时间复杂度为O(n),其中n为链表的长度。

插入和删除效率:
ArrayList:插入和删除元素时,需要将后续元素向后或向前移动,时间复杂度为O(n)。
LinkedList:由于基于链表实现,插入和删除元素只需要修改相邻元素的指针,时间复杂度为O(1)。但是在具体的位置插入和删除元素时,需要先遍历到对应位置。

内存占用:
ArrayList:由于是基于数组实现,需要预先分配一定大小的连续内存空间。如果元素数量超过数组容量,需要进行扩容操作,会占用更多的内存空间。
LinkedList:由于是基于链表实现,每个元素都包含前后指针,会消耗更多的内存空间。
根据上述区别,我们可以根据具体的应用场景选择使用ArrayList或LinkedList。如果需要频繁访问和修改元素,并且对插入和删除操作要求不高,可以选择ArrayList。如果需要频繁进行插入和删除操作,并且访问和修改元素的需求较少,可以选择LinkedList。

Override

"覆盖"(Override)和重写(override)是面向对象编程中的一个概念,指的是在子类中重新定义父类中已有的方法。

当一个子类继承自父类时,它可以使用父类中的方法。然而,有时子类需要对父类的方法进行修改或者定制化操作,这就是方法的覆盖。

要进行方法的覆盖,子类需要满足以下条件:

  1. 子类的方法名、参数列表和返回类型必须与父类中被覆盖的方法相同。
  2. 子类中的覆盖方法不能拥有比父类中被覆盖方法更为严格的访问修饰符。
  3. 子类中覆盖方法的返回类型可以是父类方法返回类型的子类。

当我们在子类中定义一个与父类中具有相同方法签名(方法名、参数列表和返回类型)的方法时,就可以认为我们在覆盖父类的方法。在运行时,当通过子类对象调用该方法时,将会执行子类中的方法而不是父类中的方法。

覆盖方法的目的通常是为了在子类中实现特定的行为,使得子类能够按照自己的逻辑来执行相同的方法名。

需要注意的是,在Java中,静态方法不能被覆盖(Override)。静态方法属于类而不是对象,并且在编译时就确定了调用的版本。所以无论如何在子类中定义相同的静态方法,都不会对父类中的静态方法产生影响。

方法重载(Method Overloading)

是指在一个类中可以存在多个同名的方法,但这些方法的参数列表不同。当调用这个方法时,编译器会根据传入的参数的类型和数量来选择匹配的方法进行调用。

方法重载的特点如下:

  1. 方法名相同:重载的方法必须使用相同的方法名。
  2. 参数列表不同:重载的方法必须有不同的参数列表,可以包括参数的类型、数量和顺序。
  3. 返回类型可以相同或不同:重载的方法可以具有相同的返回类型,也可以具有不同的返回类型,但返回类型不是方法重载的标准。
  4. 方法重载与访问修饰符和返回类型无关:重载的方法可以有不同的访问修饰符(如publicprivateprotected)和返回类型(除了void)。

方法重载的目的是提供一种更灵活的方式来处理不同类型的输入数据,使代码更简洁、易读和易于维护。通过方法重载,可以使用相同的方法名来表示一组相关的操作,而不需要在方法名中使用不同的后缀或前缀来区分。

例如,以下是一个简单的示例,演示了方法重载的使用:

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
    
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        
        System.out.println(calculator.add(2, 3));               // 调用int add(int a, int b)
        System.out.println(calculator.add(2.5, 3.7));           // 调用double add(double a, double b)
        System.out.println(calculator.add(2, 3, 5));            // 调用int add(int a, int b, int c)
    }
}

在上述示例中,Calculator类中定义了三个同名的方法add,它们的参数列表不同。通过传入不同类型和数量的参数,可以调用不同的重载方法。运行程序会输出以下结果:

5
6.2
10

通过方法重载,可以根据不同的参数类型和数量来选择正确的方法进行调用,使代码更加灵活和易于理解。

构造函数

构造函数是一种特殊的方法,用于在创建对象时进行初始化操作。它具有与类相同的名称,没有返回类型(甚至没有void),并且在使用new关键字创建对象时自动调用。

下面是一个简单的Java代码示例,展示了一个名为Person的类和其构造函数的定义:

public class Person {
    private String name;
    private int age;
    
    // 构造函数
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 其他方法
    public void sayHello() {
        System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
    }
    
    public static void main(String[] args) {
        // 使用构造函数创建Person对象
        Person person = new Person("John", 25);
        
        // 调用对象的方法
        person.sayHello();
    }
}

在上面的代码中,Person类有两个私有的成员变量name和age,以及一个公共的构造函数和一个sayHello方法。构造函数被定义为public Person(String name, int age),接受两个参数name和age,并用于初始化对象的成员变量。

在main方法中,使用构造函数new Person("John", 25)来创建一个名为person的Person对象。然后,通过调用person对象的sayHello方法,输出一个简单的问候语。

构造函数在创建对象时被自动调用,用于对对象进行初始化操作,例如给成员变量赋初始值。

深拷贝和浅拷贝

浅拷贝和深拷贝是在对象复制过程中的两种不同方式。

浅拷贝是指创建一个新对象,该对象的字段值是原始对象字段值的一份拷贝。如果字段是基本类型,拷贝的就是该字段的值;如果字段是引用类型,拷贝的就是该字段的引用,两个对象将共享同一个引用。换句话说,浅拷贝只复制对象的第一层,不会递归地复制对象的引用类型字段。

深拷贝是指创建一个新对象,并递归地复制原始对象及其所有引用类型字段所引用的对象,直到所有引用的对象都被复制。深拷贝会生成一个与原始对象完全独立的副本,即使对副本对象进行修改也不会影响原始对象。

需要注意的是,深拷贝可能会导致复杂的对象关联关系和循环引用问题,因此在实现深拷贝时需要考虑如何解决这些问题。

通常情况下,浅拷贝可以通过实现Cloneable接口并重写clone()方法来实现。而深拷贝可以通过序列化和反序列化、递归复制或使用第三方库等方式来实现。

下面是一个示例代码,演示了浅拷贝和深拷贝的区别:

class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

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

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

    public void setAddress(Address address) {
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }

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

class Address {
    private String city;

    public Address(String city) {
        this.city = city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getCity() {
        return city;
    }
}

public class Example {
    public static void main(String[] args) throws CloneNotSupportedException {
        Address address = new Address("Beijing");
        Person person1 = new Person("Alice", 20, address);

        // 浅拷贝
        Person person2 = (Person) person1.clone();
        System.out.println(person1.getAddress() == person2.getAddress()); // 输出:true,引用类型字段共享同一个引用

        // 深拷贝
        Address newAddress = new Address(person1.getAddress().getCity());
        Person person3 = new Person(person1.getName(), person1.getAge(), newAddress);
        System.out.println(person1.getAddress() == person3.getAddress()); // 输出:false,引用类型字段使用新的引用
    }
}

在上面的例子中,Person类包含一个引用类型字段address,表示人的地址。我们创建了一个person1对象,并通过浅拷贝和深拷贝分别创建了person2person3对象。

在浅拷贝中,person2address字段和person1共享同一个引用,因此它们指向的是同一个Address对象。而在深拷贝中,我们手动创建了一个新的Address对象,并将其赋给person3address字段,使得person3person1拥有不同的地址对象。

因此,浅拷贝只复制了引用类型字段的引用,而深拷贝复制了引用类型字段的整个对象。