1.面试题--Java基础

1. JAVA概述

1.1什么是Java

Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。

1.2 Java语言有哪些特点

简单易学:Java有丰富的类库,不用手写轮子

面向对象:Java是一门面向对象的语言,支持封装、继承、多态

跨平台性:Java程序可以在不同的操作系统和硬件平台上运行,实现了一次编写,到处运行的目标

安全性:Java具有高度的安全性,提供了注入类加载器、安全管理器和异常处理机制等安全机制

  Java类加载器采用双亲委派模式,即在加载类时先从父类加载器中查找对应的类,如果父类加载器中没有找到,则再去子类加载器中查找。这种机制可以防止对Java核心类库的篡改,并确保应用程序使用的是正确的类。如果你自己手写了一个Object类,这个手写的Object类是不会被加载的,而是会使用Java提供的Object类
  如果你就是想用自己写的Object类,那么需要自定义类加载器,重写其findClass方法

多线程:Java语言支持多线程编程,可以方便的实现并发操作

开放性:Java是一种开放性语言,具有开放的标准和规范,可以与其他语言进行交互和集成

高性能:Java的性能不断提高,特别是JIT编译器的引用使得Java程序的性能可以与C++等编译型语言媲美。

 当JIT编译器发现某个方法被频繁调用时,它会将该方法的字节码转换为本地机器码来提高执行速度。这是因为字节码是一种跨平台的中间代码,其性能较低,而本地机器码是针对特定硬件平台的机器指令,其性能更高。

2. 基础语法

2.1 数据类型

2.1.1 Java中的基本数据类型

数据类型大小(字节)封装类说明
byte1Byte最大存储数据量是255,存放的数据范围是-128~127之间
short2Short
int4Integer最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
long8Long最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1
float4Float
double8Double
char2Character存储Unicode码,用单引号赋值
boolean1Boolean只有true和false两个取值

一个字节代表8位。

2.1.2 3 * 0.1 == 0.3的返回值

由于浮点数在计算机中的存储方式和计算方式的特殊性,导致浮点数的运算结果可能与预期不符,故此表达式的返回值为false。

2.1.3 a = a + b和a += b的区别

+=操作会进行隐式自动类型转换,例如这里的 a += b会隐式的将加操作的结果类型强制转换为持有结果的类型,而a = a + b则不会自动进行类型转换

// 两个byte类型的变量相加时,结果会被自动提升为int类型。这种类型提升被称为"拓宽原始转换",它适用于所有原始类型,包括byte、short、char和int。
byte a = 127;
byte b = 127;
a = a + b;      // 编译报错:不兼容的类型。实际为 int',需要 'byte'
a += b;         // a = (byte)(a + b)

//案例,以下代码是否存在错误
short a = 1;
a = a + 1;
//错误,short类型在进行运算时,会自动提升为int类型,也就是说a + 1的运算结果是int类型,而a是short类型,此时编译器会报错不兼容的类型。实际为 int',需要 'short',改成+=的方式就好了
short a = 1;
a += 1;

2.1.4 Integer 和 int的区别

1.数据类型:int是Java中的一种基本数据类型,用于表示整数。它是Java语言中最常用的数据类型之一,可以直接进行数值运算,无需通过封装类进行转换。而Integer是Java中的一个封装类,是对int类型的封装,可以将int类型的数据转换为Integer类型的数据。

2.默认值:int的默认值是0,因为它是基本数据类型,在声明时会自动赋予这个值。而Integer的默认值是null,因为它是对象类型,如果没有进行实例化,则不会分配内存空间,因此其默认值为null。

3.内存存储方式:int在内存中直接存储的是数据值,这是基本数据类型的特点。而Integer实际存储的是对象引用,当使用new关键字创建一个Integer对象时,实际上是生成一个指向该对象的指针。

4.实例化:int变量在声明后无需进行实例化即可直接使用,而Integer变量在使用前必须进行实例化。

5.变量比较:int变量可以使用(双等于)操作符来比较两个变量的值是否相等。而Integer变量在比较时,由于它们是对象,所以应该使用equals()方法来比较内容是否相等。另外,对于-128到127之间的值,Java会进行缓存,所以在这个范围内的Integer对象使用(双等于)比较时,实际上是比较的引用地址,会返回true。而超出这个范围的Integer对象,即使值相等,使用“==”比较也会返回false,因为它们是不同的对象。

2.1.5 Integer和int的深入对比

1.1两个通过new生成的Integer对象,由于在堆中地址不同,所以永远不相等

1.2.int和Integer比较时,只要数值相等,结果就相等,因为包装类和基本数据类型比较时,会自动拆箱,将Integer转化为int

1.3.通过new生成的Integer对象和非通过new生成的Integer对象相比较时,由于前者存放在堆中,后者存放在Java常量池中,所以永远不相等

1.4.两个非通过new生成的Integer对象比较时,如果两个变量的数值相等且在-128到127之间,结果就相等。这是因为给Integer对象赋一个int值,java在编译时,会自动调用静态方法valueOf(),根据java api中对Integer类型的valueOf的定义,对于-128到127之间的整数,会进行缓存,如果下次再赋相同的值会直接从缓存中取,即享元模式。

//面试题:下面的代码会输出什么
public class Tmp {
    public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        Integer c = 200;
        Integer d = 200;

        System.out.println(a == b);//true
        System.out.println(c == d);//false
    }
}

输出表明a和b指向的是同一个对象,而c和d指向的不是同一个对象,我们来看一下Integer.valueOf()方法的底层源码

/**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

从注释中我们可以看到,此方法将始终缓存-128到127之间的值。

也就是如果数值在-128和127之间,就会返回IntegerCache.cache中已经存在的对象的引用,否则创建一个新的Integer对象。

所以上面的代码中,a和b的数值为100,就是从缓存中取的已存在的对象,指向的是同一个对象,所以返回true;而c和d的值为200,并不在缓存中,所以是新建的Integer对象,所以返回false.

2.1.6 Math.round(11.5) 等于多少?Math.round(-11.5)等于多少

Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。

2.1.7 最有效率的方法计算 2 乘以 8

2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)

2.1.8 ^ (异或运算)和&(与运算)

    //^ (异或运算) 相同的二进制数位上,数字相同,结果为0,不同为1。举例如下:
    0 ^ 0 = 0
    0 ^ 1 = 1
    1 ^ 1 = 0
    1 ^ 0 = 1
	// &(与运算)  相同的二进制数位上,都是1的时候,结果为1,否则为零。举例如下:
	0 & 0 = 0
	0 & 1 = 0
	1 & 0 = 0
	1 & 1 = 1

2.2编码规范

2.2.1 标识符的命名规则

标识符的含义:在程序中,我们自定义的内容,例如类的名字、方法名称、变量名称等,都是标识符

命名规则:标识符可以包含英文字母、0-9的数字、$以及_,标识符不能以数字开头,不能是关键字

命名规范:类名首字母大写,驼峰命名法。变量名、方法名首字母小写,是驼峰命名。

2.2.2访问修饰符 public,private,protected,不写(默认default)时的区别

Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问.

private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)

default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。

public : 对所有类可见。使用对象:类、接口、变量、方法.
在这里插入图片描述

2.2.3 Java注释

用于解释说明程序的文字

分类

单行注释
格式: // 注释文字
多行注释
格式: /* 注释文字 /
文档注释
格式:/
* 注释文字 */

2.3关键字

2.3.1 instanceof 关键字作用

instanceof严格来说是Java中的一个双目运算符,用来测试一个对象是否为另一个对象的实例,用法如下

boolean result = obj instance Class

其中obj为一个对象,Class表示一个类或者一个接口,当obj为Class对象,或为其子类、实现类,结果返回true,否则返回false
注意:编译器会检查obj是否能转换为右边class类型,如果不能转换则直接报错

int i = 1;
boolean res = i instanceof Integer;     // 编译不通过:不可转换的类型;无法将 'int' 转换为 'java.lang.Integer'

Integer i = new Integer(1);
boolean res = i instanceof Integer;     // true

JavaSE规范中对instanceof运算符的规定是:如果obj为null,那么返回结果总为false

boolean res = null instanceof Integer;

2.3.2 this关键字用法

引用当前对象的成员变量:当成员变量和局部变量重名时,可以使用 this 来区分它们。
引用当前对象的成员方法:同样,当成员方法和局部变量或参数重名时,可以使用 this 来区分。
在构造器中引用另一个构造器:使用 this() 来调用当前类的另一个构造器。
在方法中引用当前对象本身:可以将 this 作为参数传递给其他方法,或者返回当前对象。

public class Person {  
    private String name;  
    private int age;  
     
    public Person(String name, int age) {  
        //使用this引用成员变量name、age  
        this.name = name; 
        this.age = age;
    }  
  
    public void introduce() {  
        System.out.println("My name is " + this.name + " and I am " + this.age + " years old.");  
    }  
    
    public void introduceWithPrefix(String prefix) {  
        //使用this引用成员方法introduce
        this.introduce(); 
        System.out.println(" and I am from somewhere.");  
    }  
    
     public Person(String name) {  
        //使用this调用另一个构造器  
        this(name, 0); 
    }  
    
    public Person setName(String name) {  
        this.name = name;  
        return this; //返回引用当前对象本身 
    }  
}

注意:this 关键字不能在静态方法中使用,因为静态方法属于类本身,而不是类的实例。在静态方法中,没有当前对象的概念。

2.3.3 super关键字用法

在Java中,super关键字用于引用父类(超类)的成员变量、成员方法或构造器。

访问父类的成员变量:当子类中的成员变量与父类中的成员变量重名时,可以使用super来引用父类的成员变量。

调用父类的成员方法:当子类中的成员方法与父类中的成员方法重名时(即方法覆盖),可以使用super来调用父类的成员方法。

调用父类的构造器:在子类的构造器中,可以使用super()来调用父类的构造器。

class Parent {  
    int value = 100;  
    
     void display() {  
        System.out.println("Parent display method.");  
    } 
    
    Parent() {  
        System.out.println("Parent constructor method.");  
    }  
}  


class Child extends Parent {  
    int value = 200;  
  
    void showValue() {  
        //使用super访问父类的value=100
        System.out.println("Parent value: " + super.value); 
        // 使用this访问子类的value==200    
        System.out.println("Child value: " + this.value); 
    } 
    
    void display() {
        //使用super调用父类的display方法  
        super.display();   
        System.out.println("Child display method.");  
    } 
    
    Child() {  
        //使用super调用父类的构造器
        super();   
        System.out.println("Child's constructor called.");  
    }    

}  

public class Main {  
    public static void main(String[] args) {  
        Child child = new Child();  
    }  
}

Child类继承了Parent类,并且Child类的构造器使用super()调用了Parent类的构造器。当你创建Child类的一个对象时,会首先调用Parent类的构造器,然后调用Child类的构造器。
注意:super关键字在静态方法、静态代码块或实例初始化块中无法使用,因为这些地方并不涉及到实例的创建和初始化,所以也不涉及到父类和子类之间的关系。此外,如果父类没有定义无参数的构造器,那么在子类的构造器中必须显式地调用父类的带参数构造器

2.3.4 this与super的区别

使用场合
this关键字主要用于引用当前对象的成员变量或方法,或在构造器中调用其他构造器。
super关键字主要用于在子类中访问父类的成员变量或方法,或在子类的构造器中调用父类的构造器。

引用对象
this引用的是当前对象本身。
super引用的是当前对象的父类。

参数列表
this在调用构造方法时,参数列表是当前类的实例变量。
super在调用构造方法时,参数列表必须与父类的构造方法相匹配。

使用方式
this的使用方式是this. + 成员变量/方法/构造方法。
super的使用方式是super. + 成员变量/方法/构造方法。

super()和this()均需放在构造方法内第一行,均不可以在静态方法、静态代码块或实例初始化块中使用.

2.3.5 static用法

static是Java中的关键字,可以用来修饰类、方法、变量等,它的主要作用是创建静态成员,可以通过类名直接访问,而不需要实例化对象。

用于创建静态变量:使用static关键字定义的变量称为静态变量,它的值与所有该类的对象共享,并且可以直接通过类名访问

public class Tmp {
    static String str = "Hello";
}

public class Main {
    public static void main(String[] args) {
        System.out.println(Tmp.str);
    }
}

用于创建静态方法:使用static关键字定义的方法称为静态方法,同样可以直接通过类名调用

public class Tmp {
    static void myMethod() {
        System.out.println("Hello");
    }
}

public class Main {
    public static void main(String[] args) {
        Tmp.myMethod();
    }
}

用于创建静态代码块:使用static关键字定义的代码块称为静态代码块,它在类加载时执行,且只执行一次,一般用于初始化静态变量

public class MyClass {
    static List<String> myStaticList;
    
    static {
        // 从文件中加载数据并进行解析
        try {
            File file = new File("mydata.txt");
            BufferedReader reader = new BufferedReader(new FileReader(file));
            String line;
            myStaticList = new ArrayList<>();
            while ((line = reader.readLine()) != null) {
                myStaticList.add(line);
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        System.out.println("My static list contains: " + myStaticList);
    }
}

创建静态内部类:使用static关键字定义的内部类被称为静态内部类,它与外部类的对象无关,可以直接访问外部类的静态成员

public class OuterClass {
    private static int staticVar = 1;
    private int instanceVar = 2;

    public static class StaticInnerClass {
        public void print() {
            // 静态内部类可以直接访问外部类的静态变量
            System.out.println("StaticVar from inner class: " + staticVar);
        }
    }

    public void createInnerClass() {
        // 不需要创建OuterClass实例,但是可以直接创建StaticInnerClass实例,并且使用它访问外部类的静态成员
        StaticInnerClass staticInnerClass = new StaticInnerClass();
        staticInnerClass.print();
    }
}

2.3.5 switch中能否使用String作为参数

在jdk 1.7之前,switch只能支持byte, short, int,char或者其对应的封装类以及Enum类型作为参数。从jdk 1.7之后switch开始支持String作为参数。

2.3区别判断

2.3.1 String和StringBuilder和StringBuffer区别

在 Java 中,String、StringBuilder 和 StringBuffer 分别代表不同的字符串类型,三者底层都是字符数组来存储数据,JDK1.9之后使用的是byte[] ,因为往往我们存储都是短字符串,使用byte[]这样更节约空间。

可变性

String是只读字符串。并不是基本数据类型,而是一个对象,从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改,每次对String的操作都会生成新的String对象

private final char value[];

每次+操作:隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法,拼接+后面的字符

StringBuffer和StringBuilder都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder抽象类中我们可以看到

/**
 * The value is used for character storage.
 */
char[] value;

他们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuilder和StringBuffer来进行操作。

线程安全性

String:String类本身并不涉及线程安全的问题,因为它是不可变的。

StringBuffer:StringBuffer对方法加了同步锁或者对调用的方法加了同步锁(底层源码方法都加了synchronized),所以是线程安全的。这意味着在多线程环境下,多个线程可以同时访问和修改StringBuffer对象的内容,而不会导致数据不一致的问题。

  @Override
    public synchronized StringBuffer append(long lng) {
        toStringCache = null;
        super.append(lng);
        return this;
    }

StringBuilder:StringBuilder没有对方法进行加同步锁,所以是非线程安全的。但由于没有额外的同步开销,StringBuilder通常比StringBuffer具有更高的性能。

    @Override
    public StringBuilder append(long lng) {
        super.append(lng);
        return this;
    }

适用场景
String适用于不需要频繁修改字符串内容的场景;StringBuffer适用于需要线程安全的字符串处理场景;而StringBuilder则适用于单线程环境下的大量字符串操作,以追求更高的性能。

注意:下面是 StringBuilder 和 StringBuffer 类的一些常用方法:

append():将指定的字符串或字符序列添加到当前字符串的末尾。
insert():将指定的字符串或字符序列插入到当前字符串的指定位置处。
delete():删除当前字符串中指定位置的子串。
replace():替换当前字符串中指定位置的子串。
charAt():返回当前字符串中指定位置的字符。
substring():返回当前字符串中指定位置开始到结束位置的子串。

2.3.2 == 和 equals的区别

1.((双等于))比较两个对象的引用是否相同,也就是比较它们在内存中的地址是否相同,如果两个对象的引用相同,则返回true,否则返回false
例如下面代码中创建两个String类型的对象,他们的值相同但是引用不同,使用==比较会返回false

public class Tmp {
    public static void main(String[] args) {
        String a = new String("Hello");
        String b = new String("Hello");
        System.out.println(a == b);
    }
}

equals是比较两个对象的内容是否相同,也就是比较它们的值是否相同。如果两个对象的内容相同,则返回true,否则返回false。在Java中,Object类的equals()方法默认实现是使用==比较两个对象的引用,但可以在子类中重写该方法以实现比较对象内容的功能。
例如下面的代码中创建了两个String类型的对象,它们的值相同,所以使用equals比较会返回true

public class Tmp {
    public static void main(String[] args) {
        String a = new String("Hello");
        String b = new String("Hello");
        System.out.println(a.equals(b));
    }
}

2.3.3 Java自动装箱与拆箱

装箱就是自动将基本数据类型转换为包装类型(int -> Integer);底层调用的是Integer的valueOf(int)方法

int i = 10;
Integer i = Integer.valueOf(10);

拆箱就是自动将包装类型转换为基本数据类型(Integer -> int);底层调用的是intValue()方法

Integer i = Integer.valueOf(10); 
int j = i.valueOf(i);

2.3.4 重载和重写的区别

重载(Overload):是指在一个类中定义多个方法,它们具有相同的名称,但是参数列表(个数、类型、顺序)必须不同。重载对方法的访问权限、返回值类型和抛出的异常类型没有特殊要求。

//定义了一个名为sum的重载方法,它可以接收不同类型和数量的参数
public class MathUtils {
    public static int sum(int a, int b) {
        return a + b;
    }
    
    public static double sum(double a, double b) {
        return a + b;
    }

重写(Override):是指在子类中重新定义(覆盖)父类中已有的方法,以便实现不同的功能或适应不同的需求。重写方法必须和父类中的方法具有相同的方法名称、参数列表和返回值类型,并且访问权限不能比父类中的方法更严格.

public class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

public class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating");
    }
}

2.3.5 &和&&的区别

&和&&都是逻辑运算符,但是它们的功能有所不同。

&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作。只有两个二进制位都为1时,结果才为1,否则为0。

public class Test{
    public void main() {
        System.out.println(3 & 5);
        //3的二进制表示为11,5的二进制表示为101,进行按位与运算,得到结果为1。
    }
}

&&(逻辑与)具有短路功能。当&&左边的条件不满足时,就不会判定右边的条件

&(按位与)不具有短路功能,无论&左边的条件是否满足,它都会判定右边的条件

public class Test{
    public void main() {
        System.out.println(3>2  &&  5>6 );
        //表达式左边为true 右边的结果都为false,因此整个表达式的结果为false
    }
}

2.3.6 final finally和finalize的区别

final
概念:final是Java中的一个关键字,用于声明不可变的变量、不可继承的类或不可重写的方法。

用途:
当final修饰变量时,表示该变量的值是不可改变的,可以被视为常量。

final int MAX_VALUE = 99999;

当final修饰方法时,表示该方法不能被子类重写。

public final void myMethod(){

}

当final修饰类时,表示该类不能有子类,即该类不能被继承。

final class MyClass{
}

finally
概念:finally是Java中的一个关键字,通常与try-catch代码块一起使用。

用途:finally代码块中的代码无论是否发生异常都会被执行。它通常用于释放资源或执行一些必须执行的清理操作,如关闭数据库连接或文件流。

finalize
概念:finalize是Object类中的一个方法,用于在垃圾回收器回收对象之前执行一些清理工作。

用途:在Java中,垃圾回收器负责回收不再使用的对象并释放其占用的内存。在对象被垃圾回收器回收之前,可以使用finalize方法执行一些清理工作,如关闭文件或释放非内存资源。然而,由于Java的垃圾回收机制是自动的,并且其执行时间和顺序不受控制,因此不能保证finalize方法一定会被执行.

2.3.7 finally执行情况

在try语句块之前直接return,finally块不会执行。

在执行try语句块之前制造一个错误;在try语句块中抛出异常;在try语句块中正常执行后return

;finally块还是会执行。

2.3.8 JVM、JRE和JDK的关系

JVM是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。

JDK提供了完整的Java开发工具和环境,包括编译器、调试器和其他开发工具,用于编写、编译、调试和运行Java应用程序.它主要面向Java开发人员,他们需要使用JDK来创建Java应用程序.

JRE只提供了Java程序运行所需的环境,包括Java虚拟机(JVM)和Java类库,但不包含开发工具。它主要面向普通用户,他们只需要使用JRE来运行Java程序,而不需要进行开发或调试

2.3.9 break ,continue ,return 的区别及作用

break 跳出本次循环体,不再执行循环

continue 跳出本次循环,继续执行下次循环

return 程序返回,不再执行下面的代码

3.面向对象

3.1 面向对象概述

3.1.1 面向对象四大特性

对象:对象是类的一个实例,有状态和行为。
:类是一个模板,它描述一类对象的行为和状态。
方法:方法是类的操作的实现步骤

例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。

面向对象编程有四大特性,它们分别是:抽象、封装、继承、多态

抽象(Abstraction):指描述一类事物的共同特征和行为,抽象类不能被实例化,只能被继承,它定义了一组抽象方法,这些抽象方法必须由子类提供具体的实现。通过使用抽象类,我们可以创建出更通用的框架,并在需要时通过子类来提供具体的实现。

封装(Encapsulation):是隐藏对象的内部状态和实现细节,只对外暴露必要的接口。封装的好处在于它提供了数据的保护机制,同时使得对象的使用更加简单和方便。通过封装,我们可以减少程序中的错误,提高代码的可维护性和可读性。

继承(Inheritance):子类可以继承父类的属性和方法,从而避免了大量的重复代码。子类还可以重写父类的方法,以实现自己的特定功能。继承有助于构建层次化的类结构,使得代码的组织更加合理和清晰。

多态(Polymorphism):多态允许我们以统一的方式处理不同类型的对象。在Java中,多态主要体现在方法的重载(Overload)和重写(Override)上。重载是同一个类中有多个同名但参数列表不同的方法,而重写是子类对父类中方法的重新定义。当父类引用指向子类对象时,调用的方法会根据实际对象类型来确定,这就是多态的表现。多态使得代码更加灵活和可扩展。

这四大特性是面向对象编程的重要概念,在实际开发中需要灵活应用,以实现代码的可读性、可扩展性和可维护性。

3.1.2 多态的好处

代码复用和可维护性:多态使得开发者能够复用代码,减少重复编写。通过覆写和重载方法,可以在不修改现有代码的基础上扩展程序的功能,从而增加代码的维护性和可扩展性。

灵活性和扩展性:多态使得软件系统更易于扩展。开发者可以引入新的类对象,这些对象与现有的代码兼容,无需修改现有代码。此外,多态允许将子类的对象替换为父类的对象,进一步增加了代码的灵活性,使程序更具可定制性和可配置性。

抽象性:多态允许开发者使用抽象的接口来编程,而不是具体的实现。这种方式可以减少组件之间的依赖性,提高系统的模块化,使得修改和添加新功能时更加方便。通过将对象视为它们共同的基类或接口类型,可以在不修改现有代码的情况下扩展程序功能或实现新的功能。

动态方法调用:多态性使得方法的调用可以在运行时决定,而不是编译时。这为动态和灵活的代码执行提供了可能,允许同一个接口引用多种不同的实现。

3.1.3 代码中如何实现多态

在Java中,多态主要通过继承和接口实现,多态主要有三种表现形式:方法重载(Overload)、方法重写(Override)和接口多态

//定义一个父类
class Animal {  
    void makeSound() {  
        System.out.println("The animal makes a sound");  
    }  
}
//定义子类并重写父类方法
class Dog extends Animal {  
    @Override  
    void makeSound() {  
        System.out.println("The dog barks");  
    }  
}  
  
class Cat extends Animal {  
    @Override  
    void makeSound() {  
        System.out.println("The cat meows");  
    }  
}

//在测试类中创建对象并使用多态
public class Main {  
    public static void main(String[] args) {  
        Animal a1 = new Dog();  
        Animal a2 = new Cat(); 
          
        a1.makeSound();  
        a2.makeSound();  
    }  
}
//在这个例子中,a1和a2都是Animal类型的引用,但是它们分别引用了Dog和Cat的对象。
//当我们调用a1.makeSound()和a2.makeSound()时,实际调用的是Dog和Cat类中的makeSound()方法,这就是多态性的表现。

3.1.4 接口(interface)和抽象类(abstract)的区别

定义方式
抽象类:使用abstract关键字定义,是一个特殊的类,它不能被实例化(不能使用new关键字来创建抽象类的实例),可以包含抽象方法和非抽象方法。
接口:使用interface关键字定义,是一个完全抽象的类型,它只能包含抽象方法和常量。

实现
抽象类:如果一个类继承(通过extends关键字)了一个抽象类,那么它必须实现该抽象类中声明的所有抽象方法,除非这个子类本身也是抽象的。
接口:通过implements关键字被类实现。一个类可以实现多个接口,并必须提供所有接口中定义的抽象方法的实现。

构造函数
抽象类:可以包含构造器,这些构造器用于初始化子类对象的状态。
接口:不能有构造函数。

访问修饰符
抽象类:其方法和属性可以使用各种访问修饰符(如public、protected、private等)。
接口:其方法和属性默认是public的,且不能使用访问修饰符(在Java中)。

对继承的限制
抽象类:一个类只能直接继承自一个抽象类(在大多数面向对象编程语言中)。
接口:一个类可以实现多个接口,这是实现多重继承的一种方式。

3.1.5 父类的静态方法能否被子类重写

父类的静态方法不能被子类重写。这是因为静态方法属于类本身,而不是类的实例,因此它们与继承的关系中的动态绑定(即重写)概念是不兼容的。

3.1.6 什么是不可变对象

不可变对象是指在对象创建后,其对象的属性值不能被修改的对象。任何修改都会创建一个新的对象,如,String、Integer(对于小于或等于127的整数值)、Long(对于小于或等于127的长整数值)和BigDecimal等都是不可变对象的例子。

3.1.7 静态变量和实例变量的区别

定义与声明:静态变量在定义时前面需要加上static关键字,而实例变量则不需要。

存储位置:静态变量存储在静态变量区或方法区中,而实例变量则存储在堆内存区。

生命周期:静态变量的生命周期相对较长,它们随着类的加载而加载,直到程序结束或虚拟机停止运行时才会被销毁。实例变量的生命周期则与对象相关联,随着对象的创建而产生,随着对象的销毁而失去引用,等待垃圾回收。

访问方式:静态变量属于类级别的变量,因此它们可以通过类名直接访问。而实例变量属于对象的属性,需要通过对象来访问。

用途与共享:静态变量相当于全局变量,被所有对象共享,一旦其值被修改,所有对象都会受到影响。而实例变量只能依附于对象,作为对象的属性存在,每个对象都有自己的实例变量副本,修改一个对象的实例变量不会影响其他对象。

默认值:静态变量的默认值与其数据类型相关,例如整型默认为0,布尔型默认为false。而实例变量在创建时,如果没有显式地为其赋值,则会根据其数据类型有一个默认的初始值。

//变量类
public class Variable{  
     //静态变量1
     public static int a = 1;
     //静态变量2
     public static int b;
     //实例变量1
     public int c = 1;
     //实例变量2
     public int d;
    public static void main(String[] args) {  
         System.out.println(a);//1
         System.out.println(b);//0 
    }  
}
//测试类
public class Test{  
    public static void main(String[] args) {  
         //打印静态变量
         System.out.println(Variable.a);//1
         System.out.println(Variable.b);//0
         //创建对象
         Variable v=new Variable();
         //打印实例变量
         System.out.println(v.c);//1
         System.out.println(v.d);//0 
    }  
}

3.1.8 java创建对象的方式

1.new关键字

Variable v=new Variable();

2.克隆方法
如果一个类实现了Cloneable接口,并且覆盖了Object类中的clone()方法,那么你可以使用clone()方法来创建并返回该对象的一个拷贝。

public class MyClass implements Cloneable{  
    private int value;  
   
    public MyClass(int value) {  
        this.value = value;  
    }  
  
    // 覆盖Object类的clone()方法  
    @Override  
    protected Object clone() throws CloneNotSupportedException {  
        // 调用super.clone()创建这个对象的一个浅拷贝  
        MyClass cloned = (MyClass) super.clone();  
        return cloned;  
    }  
}
//测试
public class CloneExample {  
    public static void main(String[] args) {  
            MyClass originalObject = new MyClass(10);  
            MyClass clonedObject = (MyClass) originalObject.clone();  
}

3.使用反序列化
如果有一个对象已经被序列化到文件中,你可以通过反序列化来创建该对象的一个新实例。这通常用于保存和恢复对象的状态。

4.使用构造器方法

//测试类
public class MyClass {  
    private MyClass()
    
    //构造方法返回对象
    public static MyClass createInstance() {  
        return new MyClass();  
    }  
}  
  
MyClass myObject = MyClass.createInstance();

5.使用Constructor类的newInstance()方法创建对象

//使用Constructor类的newInstance方法创建对象 
Constructor<MyClass> constructor = MyClass.class.getConstructor(String.class);  
        MyClass obj = constructor.newInstance("Constructor called with argument"); // 

6.使用Java的依赖注入框架
在大型应用中,常使用Spring、Guice等依赖注入框架来创建和管理对象。这些框架可以自动处理对象的生命周期和依赖关系。

@Autowired  
private MyClass myObject;

7.使用枚举的单例模式

public enum Singleton {  
    INSTANCE;  
  
    public void someMethod() {
    }  
}  
  
Singleton.INSTANCE.someMethod();

3.1.9对象创建的主要流程

虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,使用”空闲列表“方式。划分内存时还需要考虑一个问题(并发),也有两种方式: CAS同步处理,或者本地线程分配缓冲(TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置,最后执行方法。

为对象分配内存

类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:

指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。

空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。即带有压缩整理功能的垃圾收集器,对象内存分配可以采用指针碰撞的方式。

处理并发安全问题

对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:

对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。

3.1.9 序列化和反序列化

序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。

反序列化就是将字节序列格式转换成对象的过程。

序列化的实现:将需要被序列化的类实现serializable接口

3.1.10 序列化中某些字段不想进行序列化,怎么解决

可以使用transient关键字修饰不想被序列化的字段,这样在序列化过程中这些字段就会被忽略掉。在反序列化时,这些字段的值会被设置成默认值,例如数值类型会被设置成0,布尔类型会被设置成false,引用类型会被设置成null。

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient int age; // transient修饰的字段
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

上面的代码中,Person类实现了Serializable接口,并且age字段被transient关键字修饰,那么在序列化过程中,age字段会被忽略掉,在反序列化时,age字段会被设置为默认值0。

import java.io.*;

public class SerializationTest {
    public static void main(String[] args) {
        Person person = new Person("John", 30);
        System.out.println("序列化前: " + person);

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) {
            Person restoredPerson = (Person) ois.readObject();
            System.out.println("序列化后: " + restoredPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

在上面的代码中,我们创建了一个Person对象,并将其序列化到文件person.dat中,然后再从文件中反序列化得到一个新的Person对象,运行结果如下

序列化前:Person [name=John, age=30]
序列化后:Person [name=John, age=0]

3.1.11 Java 支持多继承么

不支持,java中的一个类只能继承一个类,但可以实现多个接口。

3.2 深入了解

3.2.1 java当中的四种引用

强引用,软引用,弱引用,虚引用。

强引用:是最常见的引用类型,它指向一个对象,只要强引用存在,垃圾回收器就不会回收该对象,可以通过new操作符、赋值操作符或方法调用等方式创建强引用。
即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

String str = new String("Hello");

软引用:一种比较灵活的引用类型,它用来描述一些还有用,但是非必须的对象。只有当内存不足时,才会回收这些对象.

SoftReference<String> str = new SoftReference<String>(new String("Hello"));

弱引用:比弱引用还要弱一些,它指向的对象只要没有强引用指向它时,就会被回收

WeakReference<String> str = new WeakReference<>(new String("Hello"));

虚引用:必须配合引用队列一起使用,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。

PhantomReference<String> str = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());

3.2.2 Java异常Error和Exception的区别

Java中的异常分为两类:Error和Exception,二者都是Throwale类的子类。

Error表示虚拟机本身的错误或资源耗尽等严重情况,应用程序不应该视图去捕获这些异常,例如OOM(OutOfMemoryError)、SOF(StackOverFlowError)等

Exception表示程序运行中的异常情况,应该对其进行捕获和处理,Exception又分为可检查异常(Checked Exception)和不可检查异常(Unchecked Exception)

可检查异常需要程序显式地捕获并处理,例如IOException、SQLException等
不可检查异常一般是程序运行时遇到的无法处理的错误,如NullPointerException、ArrayIndexOutOfBoundsException等,这些异常都继承自RuntimeException类,也被称为运行时异常,程序不需要显式地去捕获这类异常。

3.2.3 你平时是怎样处理Java异常的

我通常遵循以下几个规则
按照异常类型分类处理:对于不同的异常类型,我会根据实际情况进行不同待处理。例如对于业务异常,我通常会将异常信息记录到日志中,并给出友好提示;对于系统异常,我会打印异常的堆栈信息,将异常信息记录到日志中以便排查问题.

异常不要吞掉:在处理异常时,我不会简单的将异常捕获并吞掉,而是尽可能的将异常处理完毕,避免出现未处理的异常导致系统不稳定或者出现非预期的问题.

日志记录:在处理异常时,我通常会将异常信息记录到日志中,以便后续的问题排查与分析.

异常处理要及时:及时处理异常可以避免问题的扩大和影响范围的扩大,同时也可以减轻排查问题的难度.

代码的健壮性:尽可能的在代码的设计和编写阶段考虑各种异常情况,图稿代码的健壮性,减少出现异常的可能性

3.2.4 获得一个类的类对象的方式

方式一:调用运行时类的属性
适用于编译期间已知的类型,它直接通过类名获取Class对象,因此,如果Person类不存在,编译器将会报错。

Class<?> clazz1 = Person.class;
System.out.println(clazz1); 

方式二:通过运行时类的对象调用getClass()
前提是已经有一个该类的实例对象。getClass()方法返回对象的运行时类的Class对象。如果对象p1为null,调用getClass()方法将会抛出NullPointerException。

Person p1 = new Person();
Class<?> clazz2 = p1.getClass();
System.out.println(clazz2);

方式三:调用Class的静态方法forName(String classPath)
forName方法允许你在运行时动态地加载一个类。它需要一个全限定类名作为参数。如果找不到指定的类,将会抛出ClassNotFoundException

Class<?> clazz3 = Class.forName("com.java.Person");  
System.out.println(clazz3);

3.2.5 泛型常用特点

Java中的泛型是一种类型参数化机制,它可以让代码更加灵活、可读性更强,同时也可以提高代码的安全性和可维护性。泛型的常用特点包括

类型安全:泛型可以让编译器在编译时就检查类型是否匹配,从而避免了很多类型转换和运行时错误

可重用性:泛型可以让同一个类或方法适用于不同的数据类型,从而提高了代码的可重用性

可读性:泛型可以让代码更易读,因为它可以让代码更具有表现力和可理解性

性能优化:泛型可以让代码更加高效,因为它可以避免在运行时进行类型转换,从俄提高了程序的性能

注意:Java中的泛型是在编译时实现的,而不是在运行时实现的。在编译时,Java编译器会进行类型擦除(Type Erasure),将泛型类型转换为普通的类型。因此,在运行时无法获取泛型类型的具体信息.

泛型在java中的实现

1.泛型类

//Box 是一个泛型类,其中 T 是一个类型参数
public class Box<T> {  
    private T item;  
  
    public Box(T item) {  
        this.item = item;  
    }  
  
    public T getItem() {  
        return item;  
    }  
  
    public void setItem(T item) {  
        this.item = item;  
    }  
}

2. 泛型接口

public interface GenericInterface<E> {  
    void operation(E item);  
}

3. 泛型方法

public interface GenericInterface<E> {  
    void operation(E item);  
}

4. 类型通配符

List<?> wildcardList = new ArrayList<String>(); // 无界通配符  
List<? extends Number> numberList = new ArrayList<Integer>(); // 有界通配符,子类型  
List<? super Integer> superList = new ArrayList<Number>(); // 有界通配符,超类型

5. 泛型与继承
泛型类和接口可以像普通类和接口一样被继承或实现。但是,子类或实现类可以引入新的类型参数,也可以继承父类或接口的类型参数。

3.3 IO流

3.3.1 说说Java中的IO流

Java中的IO流是Java提供的一种用于输入和输出数据的机制,主要分为字节流和字符流两种类型,它们可以用于读取和写入不同种类的数据源,例如文件、网络连接、内存缓冲区等。具体来说,Java中的IO流可以分为以下几种类型:

字节流(InputStream和OutStream):以字节为单位读写数据,适用于读写二进制文件和图片等数据
字符流(Reader和Writer):以字符为单位读写数据,适用于读写文本文件。

缓冲流(BufferedInputSteam、BufferedOutputSteam、BufferedReader和BufferedWriter):在字节流和字符流的基础上增加了缓冲功能,提高读写数据的效率。

对象流(ObjectInputSteam和ObjectOutputStream):用于序列化和反序列化Java对象,将Java对象转换为字节流进行存储和传输。

转换流(InputStreamReader和OutputStreamWriter):将字节流转换为字符流或将字符流转换为字节流,提供了从字节流读取Unicode字符的方法。

文件流(FileInputStream和FileOutputStream):用于读写文件,支持读写字节和字节数组。

管道流(PipedInputStream和PipedOutputStream):用于线程之间的数据传输。

通过使用不同类型的IO流,可以很方便地完成文件的读写、网络数据的传输、对象的序列化等操作

3.3.2 讲一下你对BIO,NIO,AIO的理解

BIO(Blocking I/O)

同步阻塞式 IO模型,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。。

NIO(Non-blocking I/O)

同步非阻塞 IO模型,引入了通道(Channel)和缓冲区(Buffer)的概念,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。

AIO(Asynchronous I/O)

异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。适用于高性能且高并发的场景,编程复杂度较高。

3.4 反射

3.4.1 什么是反射机制

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

静态编译和动态编译

**静态编译:**在编译时确定类型,绑定对象
**动态编译:**运行时确定类型,绑定对象

3.4.2 反射机制优缺点

优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多

3.4.3 反射机制的应用场景

模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制.

①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
1.将程序内所有 XML 或 Properties 配置文件加载入内存中;
2.Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
3.使用反射机制,根据这个字符串获得某个类的Class实例;
4.动态配置实例的属性

3.4.3 Java获取反射的三种方法

通过new对象实现反射机制
通过路径实现反射机制
通过类名实现反射机制

public class Student {
    private int id;
    String name;
    protected boolean sex;
    public float score;
}

public class Get {
    //获取反射机制三种方式
    public static void main(String[] args) throws ClassNotFoundException {
        //方式一(通过建立对象)
        Student stu = new Student();
        Class classobj1 = stu.getClass();
        System.out.println(classobj1.getName());
        //方式二(所在通过路径-相对路径)
        Class classobj2 = Class.forName("fanshe.Student");
        System.out.println(classobj2.getName());
        //方式三(通过类名)
        Class classobj3 = Student.class;
        System.out.println(classobj3.getName());
    }
}

3.5 Java实现单例模式

java中的单例模式是一种常见的设计模式,主要介绍懒汉式单例、饿汉式单例、
单例模式:通俗地说,就是一个类只能创建一个对象,并且在程序的任何地方都能够访问到该对象。

单例模式的五种实现方式:

3.5.1 饿汉式

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的。

public class Singleton1 {

  private static final Singleton1 instance = new Singleton1();
  //私有化构造方法
  private Singleton1() {}
  
  public static Singleton1 getInstance() {
      return instance;
  }
}

测试:

public class SingletonTest {

  public static void main(String[] args)  {

    System.out.println(Singleton1.getInstance());
  }
 }

3.5.2 枚举-饿汉式:

线程安全的,能防止反序列化导致重新创建新的对象保证只有一个实例

/**
 * 枚举--饿汉式
 */
public enum  Singleton2 {
    instance;

    private Singleton2(){
        System.out.println("private singleton2");
    }

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

    public static Singleton2 getInstance(){
        return instance;
    }
    public static void otherMethod(){
        System.out.println("otherMethod");
    }

}

3.5.3懒汉式

//懒汉式单例类.在第一次调用的时候实例化对象
public class Singleton3 {
    //私有化构造方法
    private Singleton() {}
    
    private static Singleton instance=null;
    
    //静态工厂方法 
    public static Singleton getInstance() {
         if (instance == null) {  
             instance = new Singleton();
         }  
        return instance;
    }
}

它是线程不安全的,并发环境下很可能出现多个Singleton实例。

/**
 * 解决方法1.在getInstance方法上加同步锁
 */
public class Singleton3 implements Serializable {

    private Singleton3(){};
    
    private static Singleton3 instance=null;
    
    public static synchronized Singleton3 getInstance(){
        if (instance==null){
            instance=new Singleton3();
        }
        return instance;
    }
}

3.5.4 双检索懒汉式

//解决方法2:双重检查锁定
public class Singleton4 implements Serializable {
  //私有化构造方法
  private Singleton4(){};
  
  private static volatile Singleton4 instance=null;//可见性、有序性
  
  //先判断实例是否存在,若不存在再对类对象进行加锁处理
  public static Singleton4 getInstance(){
    if (instance==null){
      synchronized (Singleton4.class){
        if (instance==null){
         instance=new Singleton4();
        }
      }
    }
    return instance;
  } 
}

3.5.5 内部类懒汉式(推荐):

既实现了线程安全,又避免了同步带来的性能影响。

public class Singleton5 implements Serializable {

  private  Singleton5(){};
  
  //一个私有的静态内部类,用于初始化一个静态final实例
  private static class Holder{
       private static final Singleton5 instance=new Singleton5();
  }
  
  public static Singleton5 getInstance(){
    return Holder.instance;
  }
}

就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例。

3.6 数据库

3.6.1 JDBC操作数据库的基本步骤

· JDBC操作数据库的基本步骤

· 建立数据库连接

· 创建Statement对象。

· 执行SQL语句。

· 处理查询结果。

· 关闭连接和释放资源。


public class JdbcExample {  
    public static void main(String[] args) {  
        String url = "jdbc:mysql://localhost:3306/mydatabase";  
        String user = "username";  
        String password = "password";  
        String query = "SELECT * FROM mytable";  
  
        try {  
            // 1. 加载并注册JDBC驱动  
            Class.forName("com.mysql.cj.jdbc.Driver");  
            // 2. 建立数据库连接  
            Connection connection = DriverManager.getConnection(url, user, password);  
            // 3. 创建Statement对象  
            Statement statement = connection.createStatement();  
            // 4. 执行SQL查询  
            ResultSet resultSet = statement.executeQuery(query);  
            // 5. 处理查询结果  
            while (resultSet.next()) {  
                int id = resultSet.getInt("id");  
                String name = resultSet.getString("name");  
                // 处理其他列...  
                System.out.println("ID: " + id + ", Name: " + name);  
            }  
            // 6. 关闭连接和释放资源  
            resultSet.close();  
            statement.close();  
            connection.close();  
        } catch (ClassNotFoundException e) {  
            System.err.println("JDBC Driver not found.");  
            e.printStackTrace();  
        } catch (SQLException e) {  
            System.err.println("SQL Error occurred.");  
            e.printStackTrace();  
        }  
    }  
}
}

3.6.2 Statement和PreparedStatement的区别

安全性:Statement在执行SQL语句时存在安全风险,因为它不能进行预编译,也不能使用占位符,这可能导致SQL语句拼接错误,进而容易受到SQL注入攻击。而PreparedStatement是Statement的子类,它可以预编译SQL语句,并使用占位符来避免SQL注入攻击,因此更加安全。

效率:每次使用Statement执行SQL语句时,数据库都需要重新编译该语句,这可能会消耗大量的时间和资源。而PreparedStatement支持预编译,即SQL语句在第一次执行时就被编译并存储在数据库中,后续执行时直接调用已编译的SQL语句,从而提高了执行效率。此外,PreparedStatement还支持批处理,可以一次性执行多条SQL语句,进一步提高了效率。

功能:Statement的功能相对简单,主要用于执行不带参数的SQL语句。而PreparedStatement则更加强大,它允许执行带有动态参数的SQL语句,这些参数可以在执行SQL语句之前预编译,从而提高了代码的灵活性和可读性。

3.6.3 数据库连接池的基本原理与作用

原理
数据库连接池的基本原理在于“连接复用”。在程序启动时,根据预设的配置,建立足够数量的数据库连接,并将这些连接组成一个连接池。当应用程序需要访问数据库时,它可以从连接池中获取一个已经存在的连接,而不是重新建立一个新的连接。使用完连接后,应用程序会将连接归还给连接池,而不是直接关闭它。这样,连接池中的连接可以被多次复用,从而避免了频繁创建和关闭数据库连接的开销。

作用
数据库连接池的作用主要体现在以下几个方面:

资源重用:通过复用连接,数据库连接池显著减少了创建和关闭连接的开销,从而提高了系统的整体性能。
更快的系统响应速度:由于连接池中的连接已经预先建立并处于可用状态,应用程序可以迅速获取连接并执行数据库操作,从而缩短了系统的响应时间。
控制资源使用:连接池通过限制连接的数量,防止了因过多连接而导致的系统资源浪费和负载异常。这使得系统能够在大量用户应用时保持稳定性。
统一管理:连接池提供了一种统一的连接管理方式,避免了数据库连接泄露等问题,同时也方便了监控和管理数据库连接的使用情况。

3.7 代理

3.7.1 代理模式

代理模式是一种java设计模式,它允许通过代理对象来控制对真实对象的访问。

3.7.2 静态代理

通过手动编写代理类来实现代理模式,代理类需要实现与真实类相同的接口,并在具体方法调用前后执行额外的操作。
举例说明:
1.首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。

//人员接口
public interface Person {

    /**
     * 上交班费
     */
    void giveMoney();
}

2.被代理对象实现接口,完成具体的业务逻辑

//学生类(被代理类)
public class Student implements Person {

    private String name;

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


    @Override
    public void giveMoney() {
        System.out.println(name+"上交班费100元");
    }
}

3.代理类实现接口,添加额外的操作。

//班长类(学生代理类)
public class Monitor implements Person {

    //被代理对象
    Student student;

    public Monitor(Person person) {
        //只代理学生对象
        if(person.getClass()==Student.class){
            this.student =(Student) person;
        }
    }

    //代理上交班费,调用被代理学生的上交班费行为
    @Override
    public void giveMoney() {
        System.out.println("学习有进步!");
        student.giveMoney();
    }
}

4.这里没有直接通过学生(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了,这就是代理模式。
代理模式最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。

//测试静态代理
public class StaticProxyTest {

    public static void main(String[] args) {
        //新建学生对象
        Student student = new Student("张三");
        //生成代理对象,将张三传给代理对象
        Monitor monitor = new Monitor(student);
        //代理对象执行方法
        monitor.giveMoney();
    }
}

3.7.3 动态代理

利用 Java 提供的 java.lang.reflect.Proxy 类和 InvocationHandler 接口,动态地生成代理对象,避免手动编写大量代理类。动态代理可以在运行时动态创建代理对象,根据需要代理不同的真实对象,更加灵活。

举例说明:
1.确定创建接口具体行为

//人员接口
public interface Person {

    /**
     * 上交班费
     */
    void giveMoney();
}

2…被代理对象实现接口,完成具体的业务逻辑,
加一些方法用于检测后面使用动态代理用于区分

//被代理对象
public class Student implements Person {

    private String name;

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


    @Override
    public void giveMoney() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + "上交班费100元");
    }
}

3.定义一个检测方法执行时间的工具类

public class TimeUtil {
    private static ThreadLocal<Long> tl = new ThreadLocal<>();

    //方法执行开始时间
    public static void start() {
        tl.set(System.currentTimeMillis());
        System.out.println("开始计时");
    }

    //方法执行结束时间,打印耗时
    public static void finish(String methodName) {
        long finishTime = System.currentTimeMillis();
        System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");
    }

}

4.创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。

//调用处理类
public class StuInvocationHandler<T> implements InvocationHandler {

    //被代理对象
    T target;

    public StuInvocationHandler(T target) {
        this.target = target;
    }


    /**
     * proxy:动态代理对象
     * method:正在执行的方法
     * args:调用目标方法时传入的实参
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TimeUtil.start();
        Object result = method.invoke(target, args);
        TimeUtil.finish(method.getName());
        return result;
    }
}

5.测试

//测试动态代理
public class ProxyTest {
    public static void main(String[] args) {
        //创建被代理对象
        Person student =new Student("张三");
        //创建InvocationHandler
        StuInvocationHandler stuHandler = new StuInvocationHandler<>(student);
        //Proxy.newProxyInstance()方法,返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序
        //创建代理对象,代理对象的每个执行方法都会替换执行StuInvocationHandler中的invoke方法
        Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
        //通过代理对象执行方法
        stuProxy.giveMoney();
    }
}

3.7.4 JDK动态代理和CGLIB动态代理

JDK 动态代理实现原理
1.需要一个接口来定义需要被代理的方法。
2.创建一个类,并实现接口中的方法。这个类将会被动态代理类所代理。
3.创建一个代理处理器类,实现 InvocationHandler 接口,并在 invoke() 方法中编写代理逻辑,包括对目标方法的调用前后进行额外处理。
4.通过 Proxy.newProxyInstance() 方法创建代理对象,传入目标类的类加载器、实现的接口数组和代理处理器对象。这样就得到了一个动态生成的代理类,该代理类会将所有接口方法的调用转发到代理处理器的 invoke() 方法上,然后根据反射机制调用目标类的方法。

CGlib 动态代理的实现原理

1.生成代理类:CGlib首先为目标类生成一个子类,这个子类就是代理类。这个代理类会覆盖目标类中的非final方法(final方法无法被覆盖)。
2.拦截方法调用:在代理类中,所有被覆盖的方法都被实现为调用一个特定的方法拦截器(Method Interceptor)。这个拦截器负责处理实际的方法调用。当代理对象上的方法被调用时,实际上会调用这个拦截器。
3.调用目标方法:在拦截器中,CGlib首先保存了目标对象的引用,以及被调用的方法的信息(如方法名、参数类型等)。然后,它使用反射机制调用目标对象的实际方法。
4.增强功能:在调用目标方法之前或之后,拦截器可以添加额外的逻辑,实现各种增强功能,如日志记录、性能监控、事务管理等。
5.返回结果:最后,拦截器将目标方法的返回值返回给调用者,完成整个代理过程。
举例说明:

1.定义用户接口

//用户接口
public interface UserService {

    /**
     * 新增用户
     * @param userName 用户名
     * @param password 密码
     */
    void addUser(String userName, String password);

    /**
     * 删除用户
     * @param userName 用户名
     */
    void delUser(String userName);
}

2.用户管理实现类,实现用户管理接口(被代理的实现类)

public class UserServiceImpl implements UserService {

    /**
     * 新增用户
     * @param userName 用户名
     * @param password 密码
     */
    @Override
    public void addUser(String userName, String password) {
        System.out.println("调用了用户新增的方法!");
        System.out.println("传入参数:\nuserName = " + userName +", password = " + password);
    }

    /**
     * 删除用户
     * @param userName 用户名
     */
    @Override
    public void delUser(String userName) {
        System.out.println("调用了删除的方法!");
        System.out.println("传入参数:\nuserName = "+userName);
    }

}

3.JDK动态代理实现InvocationHandler接口

//jdk动态代理调用处理类
public class JdkInvocationHandler implements InvocationHandler {

    //需要代理的目标对象
    private Object targetObject;

    /**
     * 获取代理对象
     * @param targetObject 目标对象
     * @return
     */
    public Object getJDKProxy(Object targetObject){
        //为目标对象target赋值
        this.targetObject = targetObject;
        //JDK动态代理只能针对实现了接口的类进行代理,newProxyInstance 函数所需参数就可看出
        Object proxyObject = Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
        //返回代理对象
        return proxyObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK动态代理,监听开始!");
        //调用invoke方法,result存储该方法的返回值
        Object result = method.invoke(targetObject,args);
        System.out.println("JDK动态代理,监听结束!");
        return result;
    }
}

4.导入asm版本包,实现MethodInterceptor接口

<!-- 添加asm依赖 -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>5.0.3</version>
        </dependency>

        <!-- 添加cglib依赖 -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.1</version>
        </dependency>
//Cglib动态代理调用处理类
public class CglibInvocationHandler implements MethodInterceptor {

    //代理的目标对象
    private Object target;

    /**
     * 获取代理对象
     * @param targetObject 代理的目标对象
     * @return
     */
    public UserService getCglibProxy(Object targetObject) {
        //为目标对象target赋值
        this.target = targetObject;
        Enhancer enhancer = new Enhancer();
        //设置父类,因为Cglib代理的是指定的类生成一个子类,所以需要指定父类
        enhancer.setSuperclass(targetObject.getClass());
        //设置回调
        enhancer.setCallback(this);
        //创建并返回代理对象
        Object result = enhancer.create();
        return (UserService) result;
    }

    /**
     * 重写拦截方法
     * @param o 代理对象
     * @param method 被调用方法
     * @param args 参数
     * @param methodProxy 方法代理
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib动态代理,监听开始!");
        Object result = method.invoke(target,args);
        System.out.println("Cglib动态代理,监听结束!");
        return result;
    }

}

5.测试类

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

        JdkInvocationHandler jdkHandler = new JdkInvocationHandler();
        //为UserServiceImpl实现的接口创建代理对象,而不是为UserServiceImpl类本身创建代理。
        UserService userJdkProxy = (UserService) jdkHandler.getJDKProxy(new UserServiceImpl());
        //代理对象将把方法调用分派给JdkInvocationHandler的invoke方法
        userJdkProxy.addUser("admin","123456");

        CglibInvocationHandler cglibHandler = new CglibInvocationHandler();
        //获取代理对象
        UserService userCglibProxy = cglibHandler.getCglibProxy(new UserServiceImpl());
        userCglibProxy.delUser("admin");
    }
}

JDK和CGLIB动态代理总结
JDK动态代理只能对实现了接口的类生成代理,而不能针对类 ,使用的是 Java反射技术实现,生成类的过程比较高效。
CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,使用asm字节码框架实现,相关执行的过程比较高效,生成类的过程可以利用缓存弥补,因为是继承,所以该类或方法最好不要声明成final
JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:实现InvocationHandler + 使用Proxy.newProxyInstance产生代理对象 + 被代理的对象必须要实现接口
CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理;

3.8 常见设计模式

3.8.1 单例模式(Singleton Pattern)

确保一个类仅有一个实例,并提供一个全局访问点。

public class Singleton {  
    private static Singleton instance;  
  
    private Singleton() {}  
  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

3.8.2 工厂模式(Factory Pattern)

定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

public interface Car {  
    void drive();  
}  
  
public class Sedan implements Car {  
    @Override  
    public void drive() {  
        System.out.println("Driving a Sedan");  
    }  
}  
  
public class SUV implements Car {  
    @Override  
    public void drive() {  
        System.out.println("Driving an SUV");  
    }  
}  
  
public class CarFactory {  
    public static Car createCar(String type) {  
        if ("sedan".equalsIgnoreCase(type)) {  
            return new Sedan();  
        } else if ("suv".equalsIgnoreCase(type)) {  
            return new SUV();  
        }  
        return null;  
    }  
}

3.8.3 抽象工厂模式(Abstract Factory Pattern)

提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

public interface GUI {  
    void draw();  
}  
  
public class WinGUI implements GUI {  
    @Override  
    public void draw() {  
        System.out.println("Drawing in Windows GUI");  
    }  
}  
  
public class MacGUI implements GUI {  
    @Override  
    public void draw() {  
        System.out.println("Drawing in MacOS GUI");  
    }  
}  
  
public interface Factory {  
    GUI createGUI();  
}  
  
public class WinFactory implements Factory {  
    @Override  
    public GUI createGUI() {  
        return new WinGUI();  
    }  
}  
  
public class MacFactory implements Factory {  
    @Override  
    public GUI createGUI() {  
        return new MacGUI();  
    }  
}

3.8.4 建造者模式(Builder Pattern)

将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。

public class Person {  
    private String name;  
    private int age;  
    private String address;  
  
    private Person(Builder builder) {  
        this.name = builder.name;  
        this.age = builder.age;  
        this.address = builder.address;  
    }  
  
    public static class Builder {  
        private String name;  
        private int age;  
        private String address;  
  
        public Builder withName(String name) {  
            this.name = name;  
            return this;  
        }  
  
        public Builder withAge(int age) {  
            this.age = age;  
            return this;  
        }  
  
        public Builder withAddress(String address) {  
            this.address = address;  
            return this;  
        }  
  
        public Person build() {  
            return new Person(this);  
        }  
    }  
}

3.8.5 原型模式(Prototype Pattern)

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

public class Sheep implements Cloneable {  
    private String name;  
    private int age;  
  
    public Sheep(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    @Override  
    protected Sheep clone() throws CloneNotSupportedException {  
        return (Sheep) super.clone();  
    }  
  
    public static void main(String[] args) throws CloneNotSupportedException {  
        Sheep sheep = new Sheep("Tom", 1);  
        Sheep clonedSheep = sheep.clone();  
        System.out.println(clonedSheep.name); // 输出 "Tom"  
    }  
}

3.8.6 适配器模式(Adapter Pattern)

将一个类的接口转换成客户期望的另一个接口。适配器模式使得原本由于接口不兼容而无法协同工作的类可以一起工作.

public class Target {  
    public void request() {  
        System.out.println("Called Target request()");  
    }  
}  
  
public class Adaptee {  
    public void specificRequest() {  
        System.out.println("Called Adaptee specificRequest()");  
    }  
}  
  
public class Adapter implements Target {  
    private Adaptee adaptee;  
  
    public Adapter(Adaptee adaptee) {  
        this.adaptee = adaptee;  
    }  
  
    @Override  
    public void request() {  
        adaptee.specificRequest();  
    }  
}

3.8.7 装饰器模式(Decorator Pattern)

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

3.8.8 观察者模式(Observer Pattern)

定义对象之间的一对多依赖关系,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。

3.8.9 迭代器模式(Iterator Pattern)

它提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。

3.9事务

3.9.1 什么是事务

事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。

假如小明要给小红转账100元,这个转账会涉及到两个关键操作就是:小明的余额减少1000元,将小红的余额增加1000元。如果这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

3.9.2 事务的ACID

(1).原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即事务不可分割,不可约简。
(2). 一致性(Consistency):事务开始之前和事务结束以后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
(3). 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账
(4).持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

3.9.3 事务的并发问题

脏读:(读取未提交数据)
A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。
不可重复读:(前后多次读取,数据内容不一致)
事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了修改并提交,导致事务A多次读取同一数据时,结果不一致。
幻读:(前后多次读取,数据总量不一致)
事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

3.9.4 事务隔离级别

事务的隔离级别分为:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(serializable)。

read uncommitted(读未提交): 即一个事务可以读取另一个未提交事务的数据;并发操作时会导致脏读,不能解决脏读、不可重复读、幻读。
read committed(读已提交): 允许读取并发事务已经提交的数据,解决脏读问题,并发操作会导致幻读或不可重复读。
repeatable read(可重复读): 对同一字段的多次读取结果都是一致的,不再允许修改操作,可以阻止脏读和不可重复读,但仍有可能发生幻读。
serializable(可串行化): 最高的隔离级别,所有的事务顺序执行,事务之间就完全不可能产生干扰,该级别可以防止脏读、不可重复读以及幻读。但是该级别效率低下,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别

3.9.5 如何解决事务的并发问题(脏读,幻读)

要想解决脏读、不可重复读、幻读等读现象,那么就需要提高事务的隔离级别。但与此同时,事务的隔离级别越高,并发能力也就越低。

为了解决上述问题,数据库通过锁机制解决并发访问的问题。

根据锁定对象不同:分为行级锁和表级锁;
根据并发事务锁定的关系上看:分为共享锁定和独占锁定,共享锁定会防止独占锁定但允许其他的共享锁定。而独占锁定既防止共享锁定也防止其他独占锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定,insert、update、delete和select for update语句都会隐式采用必要的行锁定。

但是直接使用锁机制管理是很复杂的,基于锁机制,数据库给用户提供了不同的事务隔离级别。

脏读的表现和具体解决并发问题

脏读就是指:A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。

解决:

修改时加排他锁,直到事务提交后才释放;

读取时加共享锁,读取完释放事务1读取数据时加上共享锁后(这样在事务1读取数据的过程中,其他事务就不会修改该数据),不允许任何事物操作该数据,只能读取,之后1如果有更新操作,那么会转换为排他锁,其他事务更无权参与进来读写,这样就防止了脏读问题。

但是当事务1读取数据过程中,有可能其他事务也读取了该数据,读取完毕后共享锁释放,此时事务1修改数据,修改完毕提交事务,其他事务再次读取数据时候发现数据不一致,就会出现不可重复读问题,所以这样不能够避免不可重复读问题。

不可重复读/ 幻读 的表现和具体解决并发问题

不可重复读:在同一事务中,前后多次读取,得到的数据内容不同
幻读:同一事务中,前后多次读取,数据总量不一致

解决:

读取数据时加共享锁,写数据时加排他锁,都是事务提交才释放锁。读取时候不允许其他事物修改该数据,不管数据在事务过程中读取多少次,数据都是一致的,避免了不可重复读问题

3.9.6 mysql怎么保证原子性

为了保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子即可。并且回滚日志会先于数据持久化到磁盘上,这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志,将之前未完成的事务重新执行。

3.9.7 说说MySQL索引,以及它们的好处和坏处

索引主要分为普通索引、唯一索引、主键索引、全文索引、组合索引几种;

好处:在大数据量的查询中,创建索引可以大幅提高系统性能,提高数据查找的效率,降低查询中分组和排序的时间,加速表与表之间的链接。

坏处:索引的存储需要占用磁盘空间,消耗存储资源;同时也增大了增删改操作的维护成本,因为每个增删改操作后相应列的索引都必须被更新

只要创建了索引,就一定会走索引吗? 不一定。 比如,在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值