知识屋:更实用的电脑技术知识网站
所在位置:首页 > 科技  > Android

Java面试笔记

发表时间:2022-03-25来源:网络

Java面试笔记

一、第一部分

1. 什么是面向对象?

答:对比面向过程,是两种不同的处理问题的角度;面向过程注重事情的每一个步骤和顺序,面向对象更注重事情有那些参与者(对象)、及各自需要做什么。

面向对象有:封装:封装的意义,在于名确标识出外部所使用的成员函数和数据项;

​ 继承:继承基类的方法,并做出自己的改变和扩展;

​ 多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。包含有继承

​ 和重写。注意:无法调用子类的特有的功能。

2. JVM性能调优

3. JDK、JRE、JVM三者区别和联系

​ 3.1. JDK:Java开发环境,由JRE和Java工具组成(javac、java、jconsole)

​ 3.2. JRE:Java运行时环境,由JVM(bin)和类库(lib)组成

​ 3.3. JVM:Java虚拟机

4. ==和equals

​ 4.1. ==:对比栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址

​ 4.2. equals:object中默认的是==比较,通常会重写。String类重写之后比较两个字符串的内容。

5. final

​ 5.1、最终的意思,简述final作用:

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

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

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

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

​ 首先需要知道的一点是:内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。

​ 将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。

​ 简单来说,就是局部变量和内部类成员变量保持一致,用final来做一个约定。

6. String、StringBuffer、StringlBuilder区别及使用场景

​ 6.1、String是final修饰的,不可变,每次操作都会产生新的String对象。

​ 6.2、StringBuffer和StringBuilder都是在原对象上操作;stringBuffer是线程安全的,StringBuilder线程不安全;StringBuffer方法都是synchronized修饰的;性能: StringBuilder > StringBuffer > String。

​ 6.3、场景:经常需要改变字符串内容时使用后面StringBuffer和StringBuilder;

​ 优先使用StringBuilder,多线程使用共享变量时使用StringBuffer。

7. 重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。方法返回值跟重载无关:

public int add(int a, String b); public String add(int a,String b); //编译会报错

重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。

8. 接口和抽象类的区别

初级:

抽象类可以存在普通成员函数(可以有实现的方法),而接口中只能存在public abstract方法。抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。抽象类只能继承—个,接口可以实现多个。

中高级:

接口的设计目的,是对类的行为进行约束(更准确的说是一种"有""约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。抽象类是对类本质的抽象,表达的是is a的关系,比如: BMw is a car。抽象类包含并实现子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。而接口是对行为的抽象,表达的是like a的关系。比如: Bird like a Aircraft(像飞行器一样可以飞),但其本质上is a Bird。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、是如何实现的,接口并不关心。使用场景:当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口。在设计阶段会降低难度

9. List和Set的区别

List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用lterator接口取得所有元素,在逐一遍历各个元素

10. hashCode与equals

​ 10.1、hashCode介绍

​ hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定 该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能根据"“键"快速的检索出对应的“值””。这其中就利用到了散列码!(可以快速找到所需要的对象)

​ 10.2、为什么要有hashCode

​ 以"Hashset如何检查重复"为例子来说明为什么要有hashCode:
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals ()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。

如果两个对象相等,则hashcode一定也是相同的两个对象相等,对两个对象分别调用equals方法都返回true两个对象有相同的hashcode值,它们也不一定是相等的因此,equals方法被覆盖过,则hashCode方法也必须被覆盖hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

二、第二部分

11、ArrayList和LinkedList区别(面试经常问的题)

​ 11.1、ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)

​ 11.2、LinkedList: 基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。

12、HashMap和HashTable的区别?底层实现是什么?

区别:

HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;HashMap允许key和value为null,而HashTable不允许

底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在

计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表key为null,存在下标0的位置

数组扩容

13、ConcurrentHashMap原理,jdk7和jdk8版本的区别

JDK1.7:

数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部锁: Segment分段锁Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的segmentget方法无需加锁,volatile保证

JDK1.8:

数据结构: synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。

读操作无锁:

Node的val和next使用volatile修饰,读写线程对该变量互相可见数组用volatile修饰,保证扩容时被读线程感知

14、如何实现一个IOC容器

配置文件配置包扫描路径递归包扫描获取.class文件反射、确定需要交给IOC管理的类对需要注入的类进行依赖注入

15、什么是字节码?采用字节码的好处是什么?

java中的编译器和解释器:

Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是解释了Java的编译与解释并存的特点。Java源代码—>编译器–>jvm可执行的Java字节码(即虚拟指令)–>jvm—>jvm中解释器---->机器可执行的二进制机器码---->程序运行。

采用字节码的好处:

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行

16、Java类加载器有哪些

JDK自带有三个类加载器: bootstrap ClassLoader、ExtClassLoader、AppClassLoader。BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件。ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。系统类加载器,线程上下文加载器。继承ExtClassLoader实现自定义类加载器

17、双亲委托模型

双亲委派模型的好处:

主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如String。同时也避免了类的重复加载,因为JⅣM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。

18、Java中的异常体系

Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常。RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。

19、GC如何判断对象可以被回收

引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以向收。可达性分析法:从 GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GCRoots没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象。

GC Roots的对象有:

虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI(即一般说的Native方法)引用的对象可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。 当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活 每个对象只能触发一次finalize()方法 由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。

20、线程的生命周期,线程有哪些状态

线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。阻塞的情况又分为三种:等待阻塞、同步阻塞、其他阻塞等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池"中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才能被唤醒,wait是object类的方法同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则VM会把该线程放入"锁池"中。其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者l/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法

三、第三部分

21、sleep()、wait()、join()、yield()的区别

1.锁池 所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源分配。 ⒉.等待池 当我们调用wait ()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify ()或notifyAll()后等待池的线程才会开始去竞争锁,notify ()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中。 sleep是Thread类的静态本地方法,wait 则是 Object类的本地方法。sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信。sleep 会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的。

yield () 执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行

join () 执行后线程进入阻塞状态,例如在线程B中调用线程A的join (),那线程B会进入到阻塞队列,直到线程A结束或中断线程

22、说说你对线程安全的理解(中级程序员)

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的

是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。

在Java中,堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。

目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。 在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

23、Thread、 Runable的区别

Thread和Runnable的实质是继承关系,一个是继承一个是实现接口。没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable。

24、说说你对守护线程的理解

可理解后台线程,不用管它。所有用户线程退出,它也会自动退出。

守护线程 :为所有非守护线程提供服务的线程;任何一个守护线程都是整个JVM中所有非守护线程的保姆;

守护线程类似于整个进程的一个默默无闻的小喽喽;它的生死无关重要,它却依赖整个进程而运行;哪天其他线程结束了,没有要执行的了,程序就结束了,理都没理守护线程,就把它中断了;

注意:由于守护线程的终止是自身无法控制的,因此千万不要把lO、File等重要操作逻辑分配给它;因为它不靠谱;

作用:GC垃圾回收线程:就是一个经典的守护线程。

应用场景

​ (1)来为其它线程提供服务支持的情况;

​ (2)或者在任何情况下,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;反之,如果一个正在执行某个操作的线程必须要正确地关闭掉否则就会出现不好的后果的话,那么这个线程就不能是守护线程,而是用户线程。通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的。

25、ThreadLocal的原理和使用场景

原理:

每一个Thread对象均含有一个ThreadLoca1Map类型的成员变量threadLocals,它存储本线程中所有ThreadLocal对象及其对应的值。 ThreadLocalMap由一个个Entry对象构成。 Entry继承自weakReference
收藏
  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜