Java面试题总结200道(二)

26、简述Spring中Bean的生命周期?

在原生的java环境中,一个新的对象的产生是我们用new()的方式产生出来的。在Spring的IOC容器中,将这一部分的工作帮我们完成了(Bean对象的管理)。既然是对象,就存在生命周期,也就是作用域。了解Spring生命周期的意义在于,可以利用Bean在其存活期间的指定时刻可以完成一些特定的任务。

一般来讲,有两种作用域的bean对象:一、singleton(单例)作用域,即意味着整个Spring容器中只会存在一个bean实例;二、prototype(原型)作用域,即每次去IOC容器中获取bean的时候都会返回一个新的实例对象;但是在基于Spring框架下的web应用里面,增加了会话维度来控制bean的生命周期:①request:针对每一次的http请求都会创建一个bean;②session:同一个session共享一个bean实例,不同的session会产生不同的bean实例对象;③globalSession:针对全局session维度共享同一个bean。

27、Spring中@Lazy注解的作用和原理?

我们在使用Spring的IOC容器进行bean对象的管理的时候,容器一启动,就会将我们所需要的bean对象(像@Component、@Configuration等注解表明的类)创建并初始化,@Lazy注解即懒加载,即在容器初始化的时候并不会去加载相应的对象(单例模式下,是可以在容器启动的时候创建对象的,而使用了@Lazy懒加载注解后,可以改变这一特性,让对象在方法调用的时候再创建)。也可以通过添加此注解的方式来解决循环依赖的问题。

对于@Lazy的依赖,其实是返回了一个代理类,而不是拿到真正的bean注入,在真正使用这个bean的时候才会去加载初始化实体类出来,所以可以解决循环依赖的问题。

28、Scope注解的作用?

@Scope注解是用来控制实例作用域的,单实例还是多实例,该注解可以作用在类和方法上面,通过属性来控制作用域,如下:

prototype :多实例,IOC容器启动的时候并不会创建对象放在容器中,每次获取的时候才会调用方法创建对象

singleton :单实例,IOC容器启动的时候就会调用方法创建对象放到容器中,以后每次获取都是从容器map中拿同一个对象

request :同一次请求创建一个实例

session:同一个session创建一个实例

29、增强for循环和for循环有什么区别?

我们先看一个简单的例子:

java 复制代码
    @Test
    public void test1(){
        String[] arr = new String[]{"a","b","c","d"};
        for (String s :arr){
            System.out.print(s+"\t");
        }
        System.out.println();
        List<String> list = Lists.newArrayList("a","b","c","d");
        for (String s : list) {
            System.out.print(s+"\t");
        }
    }

上面的例子是两个增强for循环的例子,意思很简单就是遍历然后输出每个元素。稍微有点不同的是第一个是遍历数组,第二个是遍历集合。输出结果其实也很明确,就是如下:

然后我们可以在生成的class文件中看看,

java 复制代码
    @Test
    public void test1() {
        String[] arr = new String[]{"a", "b", "c", "d"};
        String[] var2 = arr;
        int var3 = arr.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            String s = var2[var4];
            System.out.print(s + "\t");
        }

        System.out.println();
        List<String> list = Lists.newArrayList(new String[]{"a", "b", "c", "d"});
        Iterator var7 = list.iterator();

        while(var7.hasNext()) {
            String s = (String)var7.next();
            System.out.print(s + "\t");
        }

    }

遍历数组时,其实生成的class文件中还是原始的for(int i = 0; i < length-1 ;i++){}的方式,而遍历集合则使用的是迭代器。

30、final关键字的用法?

修饰类:表示类不可以被继承;

修饰方法:表示方法不可被子类覆盖,但是可以重载;

修饰变量:表示变量一旦被赋值就不可以更改它的值;

如果final修饰的是类变量(静态变量),只能在静态初始化块中进行初始化该变量,或者是声明该变量时进行赋值;

如果final修饰的是成员变量,可以在非静态初始化块,声明该变量时或者构造器中进行初始化值;

如果final修饰局部变量,需要在使用该变量之前进行显示的赋值初始化,切只能赋值一次;

如果final修饰基本类型的变量,则其数值一旦初始化便不可再进行修改;如果修饰的是引用类型的变量,则其初始化之后便不可以再指向另一个对象,但是其引用的值是可以更改的。

31、为什么局部内部类和匿名内部类只能访问局部final变量?

java 复制代码
// 匿名内部类
public class Test {

    public void test(final int b){
        final int a = 10;
        new Thread(){
          public void run(){
              System.out.println(a);
              System.out.println(b);
          }
        }.start();
    }
}
// 局部内部类
public class Test2 {
    public static void main(String[] args) {
        new Test2().outPrint(3);
    }
    private int age = 12;
    public void outPrint(final int x){
        class InClass{
            public void InPrint(){
                System.out.println(x);
                System.out.println(age);
            }
        }
        new InClass().InPrint();
    }
}

结合上述两个例子匿名内部类也好,局部内部类也罢,它和外层的Test类在编译之后其实是同级存在的,同时,匿名内部类和局部内部类并不会因为外层方法的结束而回收内部类,内部类也是一直存在的。这样就存在一个问题,根据垃圾回收的机制,外部方法结束,局部变量就会被GC回收,但是局部变量又被内部类所引用,如果局部变量被回收,那么内部类则引用了一个不存在的变量,为了解决这个问题,将局部变量copy了一份放在了内部类中,就算局部变量被回收,内部类也会一直引用局部变量的copy的副本,就像延长了局部变量的生命周期。但是还得保证局部变量和内部类中copy的副本的值一致,所以有了final关键字的引用才可以保证两处的值是一致的。

32、ArrayList和LinkedList的区别?

①、数据结构不同:ArrayList是基于数组实现的,而LinkedList是基于链表实现的;②、效率不同:Arraylist因为是基于数组实现,所以在查找元素的时候是根据下表索引去查找的,所以查询的速度很快,而Linkedlist是基于链表实现的,每个节点保存了前一个节点的地址和下一个节点的地址,所以linkedlist删除和添加元素很快,反而查询起来比较慢了;③、自由性不同:ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。④:主要开销不同:ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储节点信息以及节点指针;

ArrayList:内部使用数组的形式实现了存储,实现了RandomAccess接口,利用数组的下面进行元素的访问,因此对元素的随机访问速度非常快。

因为是数组,所以ArrayList在初始化的时候,有初始大小10,插入新元素的时候,会判断是否需要扩容,扩容的步长是0.5倍原容量,扩容方式是利用数组的复制,因此有一定的开销;

另外,ArrayList在进行元素插入的时候,需要移动插入位置之后的所有元素,位置越靠前,需要位移的元素越多,开销越大,相反,插入位置越靠后的话,开销就越小了,如果在最后面进行插入,那就不需要进行位移。

LinkedList:内部使用双向链表的结构实现存储,LinkedList有一个内部类作为存放元素的单元,里面有三个属性,用来存放元素本身以及前后2个单元的引用,另外LinkedList内部还有一个header属性,用来标识起始位置,LinkedList的第一个单元和最后一个单元都会指向header,因此形成了一个双向的链表结构。

33、ArrayList是否会越界?

会出现下标越界。

首先,ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。

对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。添加操作,首先会调用ensureCapacityInternal(size + 1),其作用为保证数组的容量始终够用,其中size是elementData数组中元组的个数,初始为0。

在ensureCapacityInternal()函数中,用if判断,如果数组没有元素,给数组一个默认大小,会选择实例化时的值与默认大小中较大值,然后调用ensureExplicitCapacity()。如果数组长度小于默认的容量10,则调用扩大数组大小的方法grow()。函数grow()解释了基于数组的ArrayList是如何扩容的。数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。

接下来回到Add()函数,继续执行,elementData[size++] = e; 这行代码就是问题所在,当添加一个元素的时候,它可能会有两步来完成:1. 在 elementData[Size] 的位置存放此元素;2. 增大 Size 的值。

在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;

而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是"线程不安全"了。这就解释了为何集合中会出现null。

但是数组下标越界还不能仅仅依靠这个来解释。我们观察发生越界时的数组下标,分别为10、15、22、33、49和73。结合前面讲的数组自动机制,数组初始长度为10,第一次扩容为15=10+10/2,第二次扩容22=15+15/2,第三次扩容33=22+22/2...以此类推,我们不难发现,越界异常都发生在数组扩容之时。

由此给了我想法,我猜想是,由于没有该方法没有同步,导致出现这样一种现象,用第一次异常,即下标为15时的异常举例。当集合中已经添加了14个元素时,一个线程率先进入add()方法,在执行ensureCapacityInternal(size + 1)时,发现还可以添加一个元素,故数组没有扩容,但随后该线程被阻塞在此处。接着另一线程进入add()方法,执行ensureCapacityInternal(size + 1),由于前一个线程并没有添加元素,故size依然为14,依然不需要扩容,所以该线程就开始添加元素,使得size++,变为15,数组已经满了。而刚刚阻塞在elementData[size++] = e;语句之前的线程开始执行,它要在集合中添加第16个元素,而数组容量只有15个,所以就发生了数组下标越界异常!

34、&和&&的区别

&运算符有两种用法:(1)按位与;(2)逻辑与。&&运算符是短路与运算。逻辑与 跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true。&&之所以称为短路运算是因为,如果&&左边的 表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。

35、当一个对象被当作参数传递到一个方法后,此方法可改变 这个对象的属性,并可返回变化后的结果,那么这里到底是值传 递还是引用传递?

是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个 参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调 用过程中被改变,但对对象引用的改变是不会影响到调用者的。

36、String、StringBuffer、StringBuilder区别

String 和 StringBuffer/StringBuilder,它 们可以储存和操作字符串。其中 String 是只读字符串,也就意味着 String 引用的 字符串内容是不能被改变的。而 StringBuffer/StringBuilder 类表示的字符串对象 可以直接进行修改。StringBuilder 和 StringBuffer 的方 法完全相同,区别在于StringBuffer的相关方法都被synchronized修饰,所以是线程安全的,而StringBuilder是非线程安全的,因此StringBuilder比StringBuffer的速度要快。

37、重载(Overload)和重写(Override)的区别。重载的 方法能否根据返回类型进行区分?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性, 而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同 的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返 回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里 氏代换原则)。重载对返回类型没有特殊的要求。所以无法根据返回类型区分重载和重写。

38、char 型变量中能不能存贮一个中文汉字,为什么?

char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode(不选择 任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一 个 char 类型占 2 个字节(16 比特),所以放一个中文是没问题的。

39、抽象类(abstract class)和接口(interface)有什么异 同?

抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如 果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实 现,否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象,因为抽象类中 可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其 中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、 public 的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接 口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类,而 抽象类未必要有抽象方法。

40、Java 中会存在内存泄漏吗

理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是 Java 被 广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无 用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。例如 Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收 这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close) 或清空(flush)一级缓存就可能导致内存泄露。

41、抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized 修饰?

都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛 盾的。本地方法是由本地代码(如 C 代码)实现的方法,而抽象方法是没有实现 的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细 节,因此也是相互矛盾的。

42、Java 中如何实现序列化,有什么意义

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流 化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。 序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会 存在数据乱序的问题)。 要实现序列化,需要让一个类实现 Serializable 接口,该接口是一个标识性接口, 标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通 过 writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要 反序列化则可以用一个输入流建立对象输入流,然后通过 readObject 方法从流中 读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆

43、Statement 和 PreparedStatement 有什么区别?哪个性 能更好?

与 Statement 相比,①PreparedStatement 接口代表预编译的语句,它主要的优 势在于可以减少 SQL 的编译错误并增加 SQL 的安全性(减少 SQL 注射攻击的可 能性);②PreparedStatement 中的 SQL 语句是可以带参数的,避免了用字符串 连接拼接 SQL 语句的麻烦和不安全;③当批量处理 SQL 或频繁执行相同的查询时, PreparedStatement 有明显的性能上的优势,由于数据库可以将编译优化后的 SQL 语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成 执行计划)。

44、事务的 ACID 是指什么

原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作 的失败都会导致整个事务的失败;

一致性(Consistent):事务结束后系统状态是一致的;

隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;

持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难 性的失败。通过日志和同步备份可以在故障发生后重建数据。

45、a = a + b 与 a += b 的区别

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

46、3*0.1 == 0.3 将会返回什么?true 还是 false?

false,因为有些浮点数不能完全精确的表示出来。浮点数不能用==判断是否相等。

47、64 位 JVM 中,int 的长度是多数?

Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就 是说,在 32 位 和 64 位 的 Java 虚拟机中,int 类型的长度是相同的。

48、"a==b"和"a.equals(b)"有什么区别?

如果 a 和 b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指 向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以 通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方 法,所以可以用于两个不同对象,但是包含的字母相同的比较。

49、a.hashCode() 有什么用?与 a.equals(b) 有什么关系?

hashCode() 方法是相应对象整型的 hash 值。它与 equals() 方法关系特 别紧密。根据 Java 规范,两个使用 equal() 方法来判断相等的对象,必须具有 相同的 hash code。

50、有没有可能两个不相等的对象有有相同的 hashcode?

有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必 须有相同的 hashcode 值,但是没有关于不相等对象的任何规定。

相关推荐
一颗花生米。15 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
问道飞鱼16 分钟前
Java基础-单例模式的实现
java·开发语言·单例模式
ok!ko4 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰5 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没5 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
小飞猪Jay6 小时前
C++面试速通宝典——13
jvm·c++·面试
编程、小哥哥6 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程7 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇7 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器