Java 面试题库

基础篇

  1.  面向对象的特征
  • 封装(Encapsulation): 封装是指将对象的数据(属性)和行为(方法)结合在一起,形成一个独立的实体。对象的数据被隐藏在内部,只能通过定义好的接口(方法)来访问。这样可以防止外界随意改变对象内部的状态,保证了数据的安全性和完整性。

  • 继承(Inheritance): 继承允许新创建的类(子类或派生类)继承现有类(父类或基类)的属性和方法。子类可以重用父类的代码,可以扩展或定制父类的功能。继承提供了代码重用的能力,并且可以建立起类之间的层次关系。

  • 多态(Polymorphism): 多态性意味着可以使用相同的接口调用不同的基于对象类型的方法。在编程中,多态通常体现为“一个接口,多个实现”,它允许将对象视为其自身的类型或其父类型的实例,并根据对象的实际类型来调用相应的方法。多态性增加了代码的灵活性和扩展性。

  • 抽象(Abstraction): 抽象是将复杂的现实问题简化为模型的过程。在面向对象编程中,抽象意味着隐藏复杂的实现细节,只展示对象的关键特性。一个抽象类或接口可以定义方法和属性的“蓝图”,具体的实现则由子类负责。

  • 2 在Java编程语言中,finalfinallyfinalize是三个有明显不同用途的概念。
  1. finalfinal是一个访问修饰符。它可以修饰类、方法和变量。

    • final修饰一个类时,表明这个类不能被继承。
    • final修饰一个方法时,表示这个方法不能被子类覆盖。
    • final修饰一个变量(包括成员变量和局部变量)时,表示这个变量的值一旦被初始化之后就不能再被改变;对于引用类型,其引用不能再指向另一个对象,但对象的内部状态是可以改变的。
  2. finallyfinally是与trycatch语句一起使用的,在异常处理中起着重要的作用。无论是否抛出或捕获异常,finally块中的代码都会被执行。因此,finally通常用于进行清理工作,如关闭文件、释放资源等。

     

    1try { 2 // 可能会抛出异常的代码 3} catch (ExceptionType name) { 4 // 处理异常 5} finally { 6 // 清理代码,无论是否抛出异常都会执行 7}

  3. finalizefinalize是一个方法,它定义在java.lang.Object类中。它在垃圾收集器确定不存在对该对象的更多引用时被垃圾收集器调用,用于执行对象销毁前的清理工作。finalize方法可以被子类覆盖以实现特定的清理逻辑。

     

    1@Override 2protected void finalize() throws Throwable { 3 try { 4 // 清理资源,如释放网络连接或关闭文件 5 } finally { 6 super.finalize(); // 调用超类的finalize方法 7 } 8}

需要注意的是,finalize方法并不推荐使用,因为它的执行时间是不确定的,且容易导致错误和性能问题。从Java 9开始,finalize方法已经被声明为过时(deprecated),因此应该避免使用,并寻找其他资源管理的方式,比如使用try-with-resources语句自动管理资源

3 int 和 Integer 有什么区别

intInteger 在Java中是两种不同的类型,它们之间的主要区别在于int是原始数据类型(primitive data type),而Integer是包装类(wrapper class)。

以下是intInteger之间的一些关键区别:

  1. int

    • int是Java的一种原始数据类型,用于表示32位有符号的整数值。
    • int类型的变量直接存储数值本身,因此它的处理效率较高。
    • 作为原始类型,int变量不能调用方法,因为它不是对象。
    • 默认值为0。
  2. Integer

    • Integerint的包装类,属于Java的一部分基本类型的包装类,位于java.lang包中。
    • Integer是一个类,因此Integer的变量实际上是一个对象的引用,这个对象包含了一个int类型的值。
    • 由于Integer是一个类,它可以有方法,例如可以调用.toString()来将整数转换成字符串,或者调用.equals()来比较两个Integer对象的值。
    • Integer对象可以为null,表示它还没有指向任何实际的整数值,这是区分于int的0值。
    • Integer类还提供了许多静态方法,例如用于解析字符串的.parseInt(),或者用于比较两个整数的.compare()

从Java 5开始,引入了自动装箱(autoboxing)和自动拆箱(unboxing)特性:

  • 自动装箱:当需要Integer对象时,原始int类型的值会自动转换为Integer对象。

     

    1int i = 10; 2Integer integer = i; // 自动装箱

  • 自动拆箱:当需要原始int类型的值时,Integer对象会自动转换为int值。

     

    1Integer integer = new Integer(10); 2int i = integer; // 自动拆箱

由于自动装箱和拆箱的存在,intInteger之间的转换在大多数情况下对开发人员是透明的,但是仍然需要注意空指针异常(NullPointerException),因为Integer对象可以为null,而int类型不能。

在实际编程过程中,选择int还是Integer取决于具体情况。如果要利用对象的特性或者需要使用集合类(如ArrayList),则必须使用Integer;但如果追求性能或者处理基本数值运算,通常会使用int

4.重载和重写的区别

重载(Overloading)和重写(Overriding)是面向对象编程中两种不同的概念,它们都允许程序员在一定程度上修改类的行为。但是,它们的用途和规则有所不同。

  1. 重载(Overloading):

    • 概念:重载是指在同一个类中定义多个名称相同但参数列表不同的方法。方法的返回类型可以相同也可以不同,并且重载与方法的访问修饰符或抛出的异常也没有关系。
    • 目的:重载使得程序员可以使用相同的方法名执行不同的功能,提高了方法的可用性。
    • 规则:要实现重载,至少要改变方法的参数数量或参数类型。只改变返回类型不足以构成重载。
    • 范围:只能在同一个类中进行。
     

    1public class Example { 2 public void display(String s) { 3 // ... 4 } 5 public void display(int i) { // 重载方法 6 // ... 7 } 8}

  2. 重写(Overriding):

    • 概念:重写是指子类重新定义父类中已有的方法。子类的方法必须与父类被重写的方法具有相同的方法名称、参数列表和返回类型。
    • 目的:重写允许子类提供特定于自己的实现,这是实现多态性的基础。
    • 规则
      • 方法名、参数列表必须完全相同。
      • 返回类型必须相同或是协变返回类型(子类重写方法的返回类型可以是父类方法返回类型的子类型)。
      • 访问修饰符可以保持相同或者更宽松,但不能更严格。
      • 重写方法不能抛出新的检查异常或者比被重写方法更广泛的检查异常。
      • 使用@Override注解可以明确指示一个方法是重写方法,有助于编译器检查和提高代码可读性。
     

    1public class Base { 2 public void display() { 3 // ... 4 } 5} 6 7public class Derived extends Base { 8 @Override 9 public void display() { // 重写方法 10 // ... 11 } 12}

综上所述,方法的重载发生在同一个类中或者在子类中,它只关心方法签名(方法名和参数列表),而不考虑方法的返回类型;方法的重写专指子类覆盖父类的同一个方法,需要保证方法签名和返回类型与被重写的父类方法一致,是实现运行时多态的关键。

Java接口默认方法:

  • 自Java 8起,接口支持定义默认方法。默认方法的定义格式为:`public default 返回值类型 方法名称(参数列表)`。这里的`public`可以略去不写,但`default`是必需的。默认方法可以在接口中定义,并且不需要实现类为其提供实现。这意味着,当一个新的接口特性被添加时,所有已存在的实现类都可以自动获得这个新的功能,而无需修改它们各自的实现代码。

  • 默认方法的使用有两种情况:

    1. 可以直接在实现类中使用接口中的默认方法,就像使用普通接口方法一样。
    2. 同样地,实现类可以对接口中的默认方法进行覆盖重写,以根据自己的需求定制逻辑。
  • 例如,以下是一个带有默认方法和实现类的简单接口示例:

    ```java

    public interface MyInterface {

    default void defaultMethod()

    }

public class DefaultTest implements MyInterface {

@Override

public String getBrand() {

return "iPhone";

}

}

```

  • 从Java 9开始,接口还可以定义静态方法和私有方法。静态方法不能通过接口实现类进行调用,而是需要通过接口名称直接调用。私有方法则是为了在不暴露具体实现细节的情况下,提供一个共用的逻辑块。

  • 接口中的常量可以被定义为`public static final`,这样的成员变量将被认为是不可变的。接口中的常量可以是数据值,也可以是构造器,但这取决于具体的编程风格和使用场景。

5.抽象类和接口有什么区别

抽象类(Abstract Class)和接口(Interface)都是Java中支持抽象概念的结构,它们都不能被实例化,可以包含抽象方法(即没有具体实现的方法)。但是,它们之间存在一些关键的区别:

  1. 设计目的

    • 抽象类通常作为多个类的共同的父类使用,用来封装这些类中的共同特征,包括字段和方法。
    • 接口则主要用来规定一个形式标准(Contract),是一种形式化的声明,它定义了类必须实现的方法,但不关注这些方法的实现细节。
  2. 方法实现

    • 抽象类可以包含抽象方法和具体方法,即可以有方法的实现,也可以没有。
    • 接口只能包含抽象方法(在Java 8之前的版本),在Java 8及之后的版本中,接口可以包含默认方法(default methods)和静态方法(static methods),默认方法可以有具体实现。
  3. 状态存储

    • 抽象类可以包含成员变量(fields),这些变量可以是非常量的,因此可以在抽象类中定义状态(state)。
    • 接口只能包含常量(在Java 8之前),即默认为public static final的变量。在Java 8及之后的版本中,接口不能包含实例字段,但可以包含静态字段。
  4. 构造函数

    • 抽象类可以包含构造函数,而接口不能。
  5. 继承和实现

    • 一个类只能继承自一个抽象类,这是因为Java不支持多重类继承。
    • 一个类可以实现多个接口,提供了一种形式的多重继承。
  6. 访问修饰符

    • 抽象类中的成员可以有多种访问修饰符,可以是public、protected或private。
    • 接口中的方法和变量默认都是public的,在Java 9及之后的版本中,接口还可以包含私有方法(private methods),这些方法是不会被继承的。
  7. 扩展性

    • 向抽象类添加新方法可能会破坏子类。
    • 接口是一种更加灵活的结构,尤其是在Java 8之后,可以通过默认方法添加新行为而不破坏实现该接口的类。

根据你的具体需求来选择使用抽象类还是接口。如果你的抽象概念更侧重于"是什么"("is-a"),那么使用抽象类;如果它更侧重于"能做什么"("can-do")或"具有什么功能"("has-a"),那么使用接口。随着Java语言的发展,接口的功能得到了增强(如默认方法),这使得接口在某些情况下可以作为抽象类的替代品。

6.说说反射的用途及实现

反射(Reflection)是一种强大的特性,允许Java程序在运行时检查或修改其自身的结构和行为。反射提供了一种机制,通过它可以在运行时获取类的信息(比如类的方法、字段、构造器等)并动态地创建对象、调用方法、访问字段和构造器等。这种动态性使得Java程序更加灵活和强大。

反射的主要用途包括:

  1. 运行时类信息获取:可以在运行时获取类的信息,例如类的名字、修饰符、包信息、父类、实现的接口、方法、字段等。

  2. 动态创建对象:可以在不知道类名的情况下创建对象实例,这对于扩展性和灵活性有很大帮助。

  3. 动态调用方法:可以在运行时调用任何方法,无需提前编码这些调用,这能够大大增加程序的灵活性。

  4. 动态访问和修改字段:可以在运行时访问和修改对象的字段,无论它们原本的访问权限如何。

  5. 实现通用代码:反射允许编写更通用的代码,这些代码可以与许多不同类型的对象一起工作。

  6. 框架和库:许多流行的框架(如Spring)和库使用反射来实现依赖注入、ORM(对象关系映射)、远程方法调用等功能。

反射的实现通常涉及以下几个步骤:

  1. 获取Class对象的引用:可以通过直接使用.class语法、调用对象的.getClass()方法或者使用Class.forName()静态方法实现。

     

    1Class cls1 = String.class; 2Class cls2 = "hello".getClass(); 3Class cls3 = Class.forName("java.lang.String");

  2. 分析类的能力:使用Class对象的方法来检索类的信息。

     

    1// 获取类的方法 2Method[] methods = cls1.getDeclaredMethods(); 3 4// 获取类的字段 5Field[] fields = cls1.getDeclaredFields(); 6 7// 获取类的构造器 8Constructor[] constructors = cls1.getDeclaredConstructors();

  3. 创建实例:使用Class对象的newInstance()方法或者获取到的构造器对象来创建类的实例。

     

    1Object obj1 = cls1.newInstance(); // Deprecated since Java 9 2Constructor constructor = cls1.getConstructor(String.class); 3Object obj2 = constructor.newInstance("constructor-arg");

  4. 访问字段和调用方法:使用FieldMethod对象来访问字段和调用方法。

     

    1// 访问字段 2Field field = cls1.getField("someField"); 3Object fieldValue = field.get(obj); 4 5// 调用方法 6Method method = cls1.getMethod("someMethod", String.class); 7method.invoke(obj, "method-arg");

  5. 修改字段的值:可以使用Field对象的set()方法来修改字段的值,即使它是私有的。

     

    1field.setAccessible(true); // 设置为可访问,忽略访问权限 2field.set(obj, "new value");

反射虽然功能强大,但也存在一些缺点:

  • 性能开销:反射操作通常比直接代码调用要慢,因为它需要在运行时分析类的元数据。
  • 安全限制:反射可能会打破封装性,改变原本私有的字段和方法,这可能会导致安全问题或者代码的不稳定。
  • 维护难度:由于反射代码的动态性,它可能难以理解和维护。

使用反射时,应该权衡利弊,并在确实需要动态性时才使用。在某些情况下,可以考虑使用其他编程技术或模式(如设计模式)来达到类似的结果,同时保持更好的性能和代码可维护性

7.说说自定义注解的场景及实现

自定义注解(Custom Annotations)在Java中是一种特殊的语法元素,它允许为代码添加元数据,这些元数据可以在编译时、类加载时或者运行时被读取,并且可以影响程序的行为。自定义注解广泛用于提供配置信息、编译检查、代码分析、测试框架等场景。

自定义注解的常见场景:

  1. 配置框架: 框架,如Spring,使用注解来配置应用程序组件和依赖注入,例如@Component@Service@Autowired等。

  2. 代码校验: 注解可用于静态代码分析工具,如检查是否遵守某些编码标准或寻找潜在的错误。

  3. 编译时处理: 创建编译时注解处理器,可以在编译时生成额外的源代码或资源文件。

  4. 测试代码: 测试框架,如JUnit,使用注解来标识测试方法(@Test)、设置测试环境(@Before@After)等。

  5. Web应用开发: Web框架使用注解来定义路由、请求和响应类型等,如JAX-RS的@Path@GET@POST等。

  6. 持久层框架: ORM框架,如Hibernate和JPA,使用注解来映射对象到数据库表。

实现自定义注解:

要实现自定义注解,需要使用@interface关键字,并且可以选择性地为注解定义成员。成员在使用注解时以键值对的形式提供,如果有默认值可以省略。

 
  

1import java.lang.annotation.*; 2 3// 定义注解的保留策略和目标 4@Retention(RetentionPolicy.RUNTIME) 5@Target(ElementType.METHOD) 6public @interface MyAnnotation { 7 // 定义注解的成员 8 String value() default "Default Value"; // 带默认值的成员 9 int number() default 0; // 另一个带默认值的成员 10}

注解成员的类型限制为原始类型、String、Class、枚举、注解以及前述类型的数组。

使用自定义注解:

自定义注解可以被应用到类、方法、字段等元素上,取决于注解的@Target定义。

 
  

1public class Example { 2 @MyAnnotation(value = "Custom Value", number = 42) 3 public void myMethod() { 4 // 方法实现 5 } 6}

读取自定义注解:

在运行时,可以通过反射API读取注解并执行相应的处理。

 
  

1Method method = Example.class.getMethod("myMethod"); 2if (method.isAnnotationPresent(MyAnnotation.class)) { 3 MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class); 4 System.out.println("value: " + myAnnotation.value()); 5 System.out.println("number: " + myAnnotation.number()); 6}

8HTTP 请求的 GET 与 POST 方式的区别

HTTP(超文本传输协议)定义了客户端与服务器之间交换数据的不同方法,其中最常用的是GET和POST请求。这两种请求方法有一些关键的区别:

  1. 用途和语义

    • GET请求通常用于请求服务器发送资源。按照HTTP规范的语义,GET请求应该是幂等的,也就是说,它不应该引起服务器状态的变化。
    • POST请求通常用于提交数据给服务器,如表单提交。与GET不同,POST请求可能会引起服务器状态的变化或副作用,比如在数据库中创建一条记录。
  2. 参数传递

    • GET请求的参数通常附加在URL之后,以查询字符串的形式出现,例如?key1=value1&key2=value2。这意味着GET请求的参数会被完全暴露在地址栏中。
    • POST请求的参数则包含在请求体中,不会显示在URL中,因此可以发送更多的数据,并且提供了更好的隐私。
  3. 数据大小

    • GET请求因为受到URL长度的限制(不同浏览器和服务器对URL长度的限制不同),因此无法发送大量数据。
    • POST请求没有URL长度限制的问题,理论上可以发送更大量的数据,因此适合表单提交、文件上传等场景。
  4. 安全性

    • GET请求由于参数在URL中,所以安全性较低,敏感信息不应该通过GET请求传送。
    • POST请求更加安全,因为数据不会保存在浏览器历史或服务器日志中。
  5. 缓存和历史

    • GET请求可以被缓存,并且会留在浏览器的历史记录中。
    • POST请求不会被缓存,通常也不会留在浏览器的历史记录中。
  6. 书签和分享

    • GET请求可以被书签,也更容易分享(复制粘贴URL)。
    • POST请求不可以直接书签,因为它包含了请求体,无法通过URL完整表示。
  7. 幂等性

    • GET请求应该是幂等的,意味着多次执行相同的GET请求,服务器上的资源状态不会改变。
    • POST请求一般不是幂等的,同样的POST请求如果多次执行,可能会每次都产生副作用(如创建多个资源)。
  8. 使用场合

    • 使用GET请求时,主要用于获取信息,例如搜索、查询、阅读页面等操作。
    • 使用POST请求时,主要用于修改服务器上的资源,例如用户注册、提交订单等操作。

总结来说,GET和POST请求在HTTP协议中有着不同的语义和使用场景。选择正确的方法将有助于遵循HTTP协议的设计意图,并确保Web应用的可靠性和效率

9.session 与 cookie 区别

Session和Cookie都是用于存储客户端和服务器交互中需要持久化的信息的机制,但它们在处理方式、存储位置和安全性等方面存在一些差异。

Cookie

  1. 存储位置:Cookie数据存储在客户端(通常是浏览器)。

  2. 存储内容:Cookie主要用于存储字符串类型的小数据量信息。

  3. 安全性:由于存储在客户端,Cookie数据容易被篡改和拦截,相对不那么安全。为了提高安全性,可以使用加密Cookie和设置HttpOnly标志。

  4. 生命周期:Cookie可以设置过期时间。如果不设置过期时间,它就是一个会话Cookie,浏览器关闭时就会被删除。设置了过期时间后,Cookie可以在本地持久化,直到过期时间。

  5. 发送请求时:每次客户端请求服务器时,Cookie都会自动添加到请求头中发送给服务器。

  6. 大小限制:Cookie有大小限制(不同浏览器有不同的限制,一般为4KB左右),而且每个域名下存储的Cookie数量也有限制。

Session

  1. 存储位置:Session数据存储在服务器端。

  2. 存储内容:Session可以存储任何类型的数据,包括对象和大量数据。

  3. 安全性:相较于Cookie,Session更安全,因为数据存储在服务器端,客户端无法直接访问。

  4. 生命周期:Session的生命周期通常由服务器控制,一般是基于用户的会话时间。用户关闭浏览器或服务器端超时设置都可能导致Session结束。

  5. 发送请求时:为了维持会话,客户端通常需要带上一个Session标识(例如,一个名为JSESSIONID的Cookie),服务器通过这个标识来查找对应的Session数据。

  6. 大小限制:Session没有大小限制,但存储过多数据会增加服务器的内存压力。

Session和Cookie的比较

  • 生命周期:Cookie可以在客户端本地存储,而Session存在于服务器上的会话时间内。

  • 安全性:Session比Cookie安全,因为Session数据存储在服务器端。

  • 存储能力:Session可以存储更多信息,而Cookie受到大小和数量的限制。

  • 服务器资源:Session数据存储在服务器上,如果大量使用将占用较多服务器资源。Cookie则存储在客户端,不占用服务器资源。

  • 跨域问题:Cookie设置时可以设置其对应的域(domain)和路径(path),实现跨域访问的控制;而Session通常只在同一域中有效,不同域的Session管理需要特殊的处理。

在实际应用中,为了结合两者的优势,经常会将Session ID存储在Cookie中,以此标识用户的会话。这样既利用了Session的安全性,又使用Cookie方便的客户端存储机制来维持会话状态

9 session 分布式处理

在分布式系统中,由于应用可能部署在多个服务器上,单机版的session管理已经不能满足需求,我们需要确保用户的会话信息在所有服务器之间同步,以便用户可以无缝地与任何服务器交互。以下是几种常见的分布式session处理方案:

  1. 粘性Session(Sticky Sessions)

    • 配置负载均衡器,使得来自同一个用户的所有请求都转发到同一台服务器。
    • 这是最简单的方法,但如果那台服务器崩溃,用户的会话信息将丢失。
  2. Session复制

    • 在服务器集群中复制session信息。
    • 每次session更新时,这些更新都会发送到集群中的其他服务器。
    • 这种方法的缺点是它会占用额外的网络带宽,并且随着集群规模的扩大,复制的成本增加。
  3. 集中式Session存储

    • 将所有session信息存储在集中的数据存储中,例如数据库或内存缓存(如Redis、Memcached)。
    • 任何服务器都可以从该存储中读取和写入session信息。
    • 这种方法的优点是可以扩展性好,而且对服务器故障有更好的容错性。
  4. 客户端Session

    • 将session数据存储在客户端,通常是以Cookie的形式。
    • 每次HTTP请求都会发送session数据,因此这种方案可能会导致数据暴露或被篡改的风险。
    • 可以通过加密和签名来增强安全性,但这会增加处理请求的复杂性。
  5. Token-based身份验证(如JWT)

    • 使用基于令牌的方法(例如JSON Web Tokens, JWT)来管理会话。
    • 服务器验证用户的身份并发放一个签名的token,客户端随后的每个请求都携带这个token。
    • 服务器通过验证token的签名来认证用户,而不需要存储会话信息。
    • 这种方法减少了服务器的存储需求,但需要额外的考虑来保证token的安全性。

实现分布式Session存储的考虑因素:

  • 一致性:需要确保所有服务器看到的session信息都是一致的。
  • 可用性:集中式session存储应该具有高可用性,以避免成为单点故障。
  • 安全性:存储session数据时需要确保数据安全,避免泄露用户敏感信息。
  • 性能:读取和写入session数据应该尽可能快,以避免降低用户体验。
  • 扩展性:随着用户数量的增长,session管理系统应该能够容易扩展。

总之,分布式session管理是构建可扩展和高可用性应用程序的关键。在实施任何分布式session解决方案时,都需要权衡不同方案的利弊,并考虑应用的具体需求和限制。

10 JDBC 流程

JDBC(Java Database Connectivity)是Java语言中用于与数据库进行交互的API,它定义了一组接口和类,使得Java应用程序能够统一地访问各种关系型数据库。使用JDBC进行数据库操作通常涉及以下步骤:

  1. 加载JDBC驱动

    • 在应用程序中加载并注册数据库的JDBC驱动,这样程序就能知道如何与特定的数据库通信。Java 6及以后的版本会自动从类路径中加载驱动,所以通常不需要显式加载驱动。
     

    1// 在Java 6之前的版本中常用的显式加载驱动的代码 2Class.forName("com.mysql.cj.jdbc.Driver");

  2. 建立数据库连接

    • 通过DriverManager类获取数据库的连接(Connection)。需要提供数据库的URL、用户名和密码。
     

    1String url = "jdbc:mysql://localhost:3306/databaseName"; 2String username = "username"; 3String password = "password"; 4Connection conn = DriverManager.getConnection(url, username, password);

  3. 创建Statement对象

    • 通过Connection对象创建StatementPreparedStatementCallableStatement对象。Statement用于执行静态SQL语句,PreparedStatement用于执行预编译的SQL语句,而CallableStatement用于执行存储过程。
     

    1Statement stmt = conn.createStatement(); 2PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM table WHERE column = ?"); 3CallableStatement cstmt = conn.prepareCall("{call stored_procedure(?, ?)}");

  4. 执行SQL语句

    • 使用Statement对象执行SQL语句。根据操作类型的不同,可能会调用executeQuery(用于SELECT查询)、executeUpdate(用于INSERT、UPDATE、DELETE语句)或execute(对于更通用的情况)方法。
     

    1ResultSet rs = stmt.executeQuery("SELECT * FROM table"); 2int rowsAffected = pstmt.executeUpdate(); 3boolean hasResults = cstmt.execute();

  5. 处理结果集

    • 对于查询操作,执行SQL语句会返回一个ResultSet对象,程序需要遍历这个对象来处理查询结果。
     

    1while (rs.next()) { 2 String data = rs.getString("column_name"); 3 // 处理获取到的数据 4}

  6. 关闭资源

    • 使用完数据库资源后,应该关闭ResultSetStatementConnection对象,释放占用的资源。最好在finally块中或使用try-with-resources语句来确保这些资源总是被正确关闭。
     

    1// 使用try-with-resources语句自动关闭资源 2try (Connection conn = DriverManager.getConnection(url, username, password); 3 Statement stmt = conn.createStatement(); 4 ResultSet rs = stmt.executeQuery("SELECT * FROM table")) { 5 while (rs.next()) { 6 // 处理结果集 7 } 8} catch (SQLException e) { 9 e.printStackTrace(); 10}

以上就是一个典型的JDBC操作数据库的流程。在实际应用中,JDBC代码通常会更加复杂,需要处理异常、事务控制、连接池管理等。因此,很多开发者会选择使用ORM框架(如Hibernate、MyBatis)来简化数据库操作,这些框架内部依然使用JDBC技术与数据库通信。

11.MVC 设计思想

MVC(Model-View-Controller)设计思想是一种软件架构模式,它将应用程序分为三个相互协作的部件或层,以实现业务逻辑、数据处理和用户界面展示之间的解耦。每个部分负责不同的职责:

模型(Model):

模型层代表了应用程序的核心业务逻辑和数据结构。
它包含了所有与应用状态和数据相关的操作,如数据库访问、业务规则验证、数据计算等。
当模型的状态发生变化时,通常会通知相关视图进行更新。
视图(View):

视图层是用户看到并与之交互的界面部分。
视图从模型获取数据并将其呈现给用户,但它不直接操作数据,而是显示由模型提供的数据。
视图通常依赖于模型的数据变化,通过监听或数据绑定机制来自动刷新界面。
控制器(Controller):

控制器层负责接收用户的输入请求,并根据请求执行相应的业务逻辑。
控制器解析用户的操作,调用模型的方法来处理数据,并决定应该向哪个视图传递这些结果或触发什么样的视图更新。
在Web开发中,控制器类通常使用注解如@Controller或@RestController来定义路由处理函数,它们作为HTTP请求和响应处理的入口点。
采用MVC设计模式的好处包括:

分离关注点:各层专注于自己的功能,便于代码维护和扩展。
可重用性:视图可以独立于模型和控制器改变,反之亦然。
灵活性:可以根据需求更改视图的表现形式而无需修改模型或底层业务逻辑。
测试方便:各个组件可以独立测试,降低了模块间相互影响导致的测试复杂度。
在现代Web框架(如Spring MVC、ASP.NET MVC等)中,MVC模式得到了广泛应用和发展,虽然具体的实现细节可能有所不同,但核心理念仍然保持一致。

12.equals 与 == 的区别

在 Java 中,equals 方法和 == 操作符都用于比较两个对象,但它们的比较方式和使用场景有所不同。

== 操作符

  • 基本类型:当用于基本数据类型(如 intchardouble 等)时,== 比较的是值是否相等。
  • 引用类型:当用于引用类型(如对象)时,== 比较的是两个引用是否指向内存中的同一个对象实例。

例如:

 
  

1String str1 = new String("example"); 2String str2 = new String("example"); 3boolean result = (str1 == str2); // 结果为 false,因为 str1 和 str2 指向不同的对象实例

equals 方法

  • equals 是 Object 类的一个非静态方法,它用于检查两个对象的内容是否相等。默认实现(即 Object 类中的实现)是使用 == 操作符来比较两个对象的引用。
  • 通常,当创建一个类时,如果需要基于对象的内容而不是对象引用来判断相等性,就应当重写 equals 方法。
  • 例如,String 类重写了 equals 方法,使得它可以比较两个字符串的内容是否相等。
 
  

1String str1 = new String("example"); 2String str2 = new String("example"); 3boolean result = str1.equals(str2); // 结果为 true,因为 str1 和 str2 的内容相同

比较

  • == 用于基本类型的值比较和引用类型的引用比较。
  • equals 用于比较两个对象的内容是否相等,但需要注意,如果没有在类中重写 equals 方法,该方法的行为默认是比较对象的引用(与 == 相同)。

重要点

当重写 equals 方法时,应该始终遵守以下约定,以确保该方法的行为符合开发者的期望:

  • 自反性:对于任何非空引用值 xx.equals(x) 应该返回 true
  • 对称性:对于任何非空引用值 x 和 yx.equals(y) 应该返回 true 当且仅当 y.equals(x) 返回 true
  • 传递性:对于任何非空引用值 xy 和 z,如果 x.equals(y) 返回 true 并且 y.equals(z) 返回 true,那么 x.equals(z) 也应该返回 true
  • 一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 应该一致地返回 true 或一致地返回 false,前提是对象上的信息没有被修改。
  • 对 null 的处理:对于任何非空引用值 xx.equals(null) 应该返回 false

在重写 equals 方法时,通常也需要相应地重写 hashCode 方法,以保持 equalshashCode 之间的一致性合同,这对于将对象作为散列集(如 HashSet)中的键来说是必要的。

13. string、StringBuilder、StringBuffer的区别

在 Java 中,StringStringBuilderStringBuffer 都用于处理字符串,但它们在功能和性能方面有所不同。

String

  • String 类表示不可变的字符序列。在 Java 中,每次对字符串进行修改(例如通过拼接、替换等操作),实际上都会创建一个新的 String 对象,而不是修改原有对象。由于字符串不可变,它们在多线程环境下是线程安全的。

  • 由于 String 对象不可变,频繁的字符串操作可能会导致内存和性能开销。例如:

     

    1String s = "Hello"; 2s += " World"; // 这里实际上创建了一个新的 String 对象

StringBuilder

  • StringBuilder 类表示可变的字符序列。它是 Java 5 引入的,提供了一系列用于操作字符串内容的方法,例如 appendinsertdelete 等。由于 StringBuilder 是可变的,它可以在不生成新对象的情况下修改字符串。

  • StringBuilder 不是线程安全的。在单线程环境中,对性能要求较高的字符串操作通常应该使用 StringBuilder。例如:

     

    1StringBuilder sb = new StringBuilder("Hello"); 2sb.append(" World"); // 直接在原有字符串的基础上追加,没有创建新对象

StringBuffer

  • StringBuffer 类与 StringBuilder 类似,也表示可变的字符序列,提供了类似的字符串操作方法。不同之处在于 StringBuffer 是线程安全的,所有公共方法都是同步的,这意味着它可以在多线程环境中安全使用。

  • 由于 StringBuffer 的线程安全特性,它通常比 StringBuilder 慢。如果不需要线程安全,通常建议使用 StringBuilder,因为它的性能更优。例如:

     

    1StringBuffer sbf = new StringBuffer("Hello"); 2sbf.append(" World"); // 线程安全的追加操作

总结

  • String:不可变字符串类,简单使用时最为方便,但不适用于大量频繁的字符串修改操作。
  • StringBuilder:可变字符串类,适用于单线程下需要频繁修改字符串的场景,性能优于 StringBuffer
  • StringBuffer:可变字符串类,线程安全,适用于多线程下需要频繁修改字符串的场景。

在选择使用 StringStringBuilderStringBuffer 时,应当根据实际需求和上下文来做出决策。通常在循环、大量字符串操作或拼接时,选择 StringBuilderStringBuffer,而在字符串相对固定且操作较少的场合使用 String
 

14.List 和 Set 区别

Java中的List和Set都是集合框架的一部分,它们都继承自Collection接口,但具有不同的特性:

列表(List):

特性:有序、可重复
插入顺序与迭代顺序相同。
支持通过索引访问元素,因此可以快速进行随机访问。
允许包含多个null元素(但不是推荐的做法)。
常用实现类包括:ArrayList、LinkedList、Vector等。
ArrayList基于动态数组实现,查询快,增删慢;LinkedList基于双向链表实现,查询慢,增删快。
集合(Set):

特性:无序(不保证插入顺序)、不可重复
不支持索引访问,仅能通过迭代器遍历元素。
只允许包含一个null元素(在某些实现中,如HashSet,可能不允许任何null值)。
Set中的元素唯一性由其equals()方法决定。
常用实现类包括:HashSet、TreeSet、LinkedHashSet等。
HashSet基于哈希表实现,检索效率高,插入删除效率也较高,并且不保证元素的插入顺序;TreeSet基于红黑树实现,自动排序元素(根据自然顺序或自定义比较器),插入删除操作比HashSet慢些,但是提供了排序功能;LinkedHashSet结合了HashSet和LinkedList的特点,它保持元素的插入顺序并且不允许重复。
总结来说,如果你需要维护元素的顺序和允许重复元素,那么选择List;如果你关心元素的唯一性而不关注它们的存储顺序(或者有特定排序需求),则应该使用Set。

15.List 和 Map 区别

Java中的List和Map是两种不同类型的集合,它们分别用于存储不同类型的数据结构,并且具有不同的功能和使用场景:

  1. 列表(List)

    • 类型:有序、可重复的元素序列。
    • 特性:
      • 元素是有序的,即插入顺序与迭代顺序相同(除非进行了排序操作)。
      • 支持索引访问,可以通过下标快速获取或修改指定位置的元素。
      • 可以包含重复元素,只要equals()方法判断为不相等即可。
      • 常用实现类包括ArrayList、LinkedList和Vector。
  2. 映射(Map)

    • 类型:键值对集合,其中每个键与一个值相关联。
    • 特性:
      • 键不可重复,根据键唯一性来确定元素的唯一性(键的hashCode()和equals()方法共同决定键的唯一性)。
      • 不支持索引访问,而是通过键来查找对应的值。
      • 值可以重复,即使多个键对应相同的值也是允许的。
      • Map中元素没有明确的顺序,尽管某些实现如LinkedHashMap会按照插入顺序或最近最少使用(LRU)顺序进行迭代。
      • 常用实现类包括HashMap、TreeMap、LinkedHashMap等。

总结来说,List适用于需要维护元素顺序并且可能有重复数据的场景;而Map则适用于需要通过唯一的键来关联和检索值的场景,它不关心元素的顺序(除非特殊实现),并且确保键的唯一性。

16.Arraylist 与 LinkedList 区别

ArrayList:

ArrayList 是基于动态数组的数据结构。它允许我们快速地通过索引访问元素,因为数组使得随机访问变得容易。
当 ArrayList 的元素个数超出当前容量时,它需要进行数组的扩容操作(通常是将当前数组大小增加一半),这涉及到创建新的数组并复制旧数组的内容,这个过程开销较大。
LinkedList:

LinkedList 是基于双向链表的数据结构。每个元素都是一个节点,每个节点都包含了前一个和后一个元素的引用。
LinkedList 适合于频繁的插入和删除操作,因为链表不需要重新调整数组大小,只需修改节点的引用即可。
性能
随机访问:

ArrayList 在随机访问元素方面性能很好,因为数组提供了直接通过索引访问的能力。
LinkedList 在随机访问方面性能较差,因为需要从头或尾开始遍历链表直到找到目标元素。
插入和删除:

ArrayList 在插入和删除元素时性能较差,特别是在列表的开头或中间,因为需要移动插入点之后的所有元素。
LinkedList 在插入和删除操作方面性能较好,因为这些操作只涉及更改节点的引用。
内存开销
ArrayList 因为其基于数组的特性,相对来说内存使用更加紧凑。但是在扩容时可能会有额外的内存开销,且扩容操作频繁时会影响性能。
LinkedList 对于每个元素都需要额外存储两个引用(前一个和后一个节点),所以相比 ArrayList 有更大的内存开销。
使用场景
ArrayList 适合于那些主要进行随机访问操作的场景,同时对插入和删除操作的性能要求不高。
LinkedList 适合于那些需要频繁进行插入和删除操作的场景,特别是在列表的两端进行操作,但不需要频繁的随机访问。
API 扩展
LinkedList 实现了 List 接口之外的 Deque 接口,提供了在列表的头尾进行插入、删除的额外方法,如 addFirst()、addLast()、removeFirst() 和 removeLast()。
总结来说,ArrayList 和 LinkedList 在不同的使用场景下各有优势。开发者应根据具体需求选择最合适的实现类。

你可能感兴趣的:(java,面试,开发语言)