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

Android 高频面试必问之Java基础

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

如果大家去面Android客户端岗位,Java基础是必问的环节,所以我给大家总结下Android面试中会被问到的一些Java基础知识。

1,面向对象和面向过程的区别

面向过程:面向过程性能比面向对象高。因为对象调用需要实例化,开销比较大,较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等,一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。 面向对象:面向对象易维护、易复用、易扩展。因为面向对象有封装、继承、多态性的特性,所以可设计出低耦合的系统,使得系统更加灵活、更加易于维护。

那为什么,面向过程性能比面向对象高呢? 面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是因为 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机器码。而面向过程语言大多都是直接编译成机器码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

2,面向对象的特征有哪些

封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。即同一消息可以根据发送对象的不同而采取不同的行为方式。

3,解释下Java的编译与解释并存的现象

当 .class 字节码文件通过 JVM 转为机器可以执行的二进制机器码时,JVM 类加载器首先加载字节码文件,然后通过解释器逐行进行解释执行,这种方式的执行速度相对比较慢。而且有些方法和代码块是反复被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成一次编译后,会将字节码对应的机器码保存下来,下次可以直接调用。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。

4,简单介绍下JVM的内存模型

Java虚拟机所管理的内存包含程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区5个部分,模型图如下图所示。

4.1 程序计数器

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,这类内存区域为【线程私有】的内存。

程序计数器具有如下的特点:

是一块较小的内存空间。线程私有,每条线程都有自己的程序计数器。生命周期方面,随着线程的创建而创建,随着线程的结束而销毁。是唯一一个不会出现OutOfMemoryError的内存区域。

4.2 Java虚拟机栈

Java虚拟机栈也是线程私有的,它的生命周期与线程的生命周期同步,虚拟机栈描述的是Java方法执行的线程内存模型。每个方法被执行的时候,Java虚拟机都会同步创建一个内存块,用于存储在该方法运行过程中的信息,每个方法被调用的过程都对应着一个栈帧在虚拟机中从入栈到出栈的过程。

Java虚拟机栈有如下的特点:

局部变量表所需的内存空间在编译期间完成分配,进入一个方法时,这个方法需要在栈帧中分配的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。Java虚拟机栈会出现两种异常:StackOverflowError 和 OutOfMemoryError。

4.3 本地方法栈

本地方法栈与虚拟机所发挥的作用很相似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地方法服务。

4.4 Java堆

Java堆是虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,java中“几乎”所有的对象实例都在这里分配内存。这里使用“几乎”是因为java语言的发展,及时编译的技术发展,逃逸分析技术的日渐强大,栈上分配、标量替换等优化手段,使java对象实例都分配在堆上变得不那么绝对。 Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法(G1之后开始变得不一样,引入了region,但是依旧采用了分代思想),Java堆中还可以细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,简写TLAB)。

OOM异常 Java堆的大小既可以固定也可以扩展,但是主流的虚拟机,堆的大小都是支持扩展的。如果需要线程请求分配内存,但堆已满且内存已无法再扩展时,就抛出 OutOfMemoryError 异常。比如:

/** * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOMTest { public static final int _1MB = 1024 * 1024; public static void main(String[] args) { List list = new ArrayList(); for (int i = 0; i 直接内存 -> 本地 IO堆内存作用链:本地 IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地 IO

服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

5,简单介绍下Java的类加载器

Java的类加载器可以分为BootstrapClassLoader、ExtClassLoader和AppClassLoader,它们的作用如下。

BootstrapClassLoader:Bootstrap 类加载器负责加载 rt.jar 中的 JDK 类文件,它是所有类加载器的父加载器。Bootstrap 类加载器没有任何父类加载器,如果调用String.class.getClassLoader(),会返回 null,任何基于此的代码会抛出 NUllPointerException 异常,因此Bootstrap 加载器又被称为初始类加载器。ExtClassLoader:Extension 将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性定义的目录下加载类。Extension 加载器由 sun.misc.Launcher$ExtClassLoader 实现。AppClassLoader:Java默认的加载器就是 System 类加载器,又叫作 Application 类加载器。它负责从 classpath 环境变量中加载某些应用相关的类,classpath 环境变量通常由 -classpath 或 -cp 命令行选项来定义,或者是 JAR 中的 Manifest 的 classpath 属性,Application 类加载器是 Extension 类加载器的子加载器。

类加载会涉及一些加载机制。

委托机制:加载任务委托交给父类加载器,如果不行就向下传递委托任务,由其子类加载器加载,保证Java核心库的安全性。可见性机制:子类加载器可以看到父类加载器加载的类,而反之则不行。单一性原则:父加载器加载过的类不能被子加载器加载第二次。

6,谈一下Java的垃圾回收,以及常用的垃圾回收算法。

Java的内存管理主要涉及三个部分:堆 ( Java代码可及的 Java堆 和 JVM自身使用的方法区)、栈 ( 服务Java方法的虚拟机栈 和 服务Native方法的本地方法栈 ) 和 保证程序在多线程环境下能够连续执行的程序计数器。 Java堆是进行垃圾回收的主要区域,故其也被称为GC堆;而方法区的垃圾回收主要针对的是新生代和中生代。总的来说,堆 (包括Java堆 和 方法区)是 垃圾回收的主要对象,特别是Java堆。

6.1 垃圾回收算法

6.1.1 对象存活判断

引用计数

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法虽然简单,但无法解决对象相互循环引用的问题。

可达性分析

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。在Java中,GC Roots包括:

虚拟机栈中引用的对象。方法区中类静态属性实体引用的对象。方法区中常量引用的对象。本地方法栈中 JNI 引用的对象。

6.2 垃圾收集算法

标记清除法

如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。 标记复杂算法有两个主要的缺点:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 它的优点是每次只需要对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。而缺点也是显而易见的,内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记整理法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法 分代收集算法,就是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

7,成员变量和局部变量的区别

从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public、private、static 等修饰符所修饰,而局部变量不能被这些修饰符所修饰;但是它们都可以被 final 所修饰。从变量在内存中的存储方式来看:如果成员变量被 static 所修饰,那么这个成员变量属于类,如果没有被 static 修饰,则该成员变量属于对象实例。对象存在于堆内存,局部变量存在于栈内存(具体是Java虚拟机栈)。从变量在内存中的生存时间来看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用结束而自动消失。成员变量如果没有赋初始值,则会自动以类型的默认值而赋值(例外:被 final 修饰的成员变量必须在初始化时赋值),局部变量则不会自动赋值。

8,Java 中的方法重写(Overriding)和方法重载(Overload)的含义

方法重写 在Java程序中,类的继承关系可以产生一个子类,子类继承父类,它具备了父类所有的特征,继承了父类所有的方法和变量。子类可以定义新的特征,当子类需要修改父类的一些方法进行扩展,增大功能,程序设计者常常把这样的一种操作方法称为重写,也叫称为覆写或覆盖。

方法重写有如下一些特点:

方法名,参数列表必须相同,返回类型可以相同也可以是原类型的子类型重写方法不能比原方法访问性差(即访问权限不允许缩小)。重写方法不能比原方法抛出更多的异常。重写发生在子类和父类之间。重写实现运行时的多态性。

方法重载 方法重载是让类以统一的方式处理不同类型数据的一种手段。调用方法时通过传递给它们的不同个数和类型的参数来决定具体使用哪个方法,这就是多态性。所谓方法重载是指在一个类中,多个方法的方法名相同,但是参数列表不同。参数列表不同指的是参数个数、参数类型或者参数的顺序不同。

方法名必须相同,参数列表必须不同(个数不同、或类型不同、参数类型排列顺序不同等)。方法的返回类型可以相同也可以不相同。重载发生在同一类中。重载实现编译时的多态性。

9,简单介绍下传递和引用传递

按值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。简单来说就是直接复制了一份数据过去,因为是直接复制,所以这种方式在传递时如果数据量非常大的话,运行效率自然就变低了,所以Java在传递数据量很小的数据是值传递,比如Java中的各种基本类型:int、float、double、boolean等类型。

引用传递:引用传递其实就弥补了上面说的不足,如果每次传参数的时候都复制一份的话,如果这个参数占用的内存空间太大的话,运行效率会很底下,所以引用传递就是直接把内存地址传过去,也就是说引用传递时,操作的其实都是源数据,这样的话修改有时候会冲突,记得用逻辑弥补下就好了,具体的数据类型就比较多了,比如Object,二维数组,List,Map等除了基本类型的参数都是引用传递。

10,为什么重写 equals 时必须重写 hashCode 方法

下面是使用hashCode()与equals()的相关规定:

如果两个对象相等(即用 equals 比较返回 true),则 hashcode 一定也是相同的;两个对象有相同的 hashcode 值,它们也不一定是相等的(不同的对象也可能产生相同的 hashcode,概率性问题);equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

为什么必须要重写 hashcode 方法?其实就是为了保证同一个对象,保证在 equals 相同的情况下 hashcode 值必定相同,如果重写了 equals 而未重写 hashcode 方法,可能就会出现两个没有关系的对象 equals 相同的(因为 equals 都是根据对象的特征进行重写的),但 hashcode 确实不相同的。

11,接口和抽象类的区别和相同点是什么

相同点

接口是绝对抽象的,不可以被实例化,抽象类也不可以被实例化。类可以不实现抽象类和接口声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。

异同点:

从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。定义接口的关键字是 interface ,抽象类的关键字是 abstract class接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。类可以实现很多个接口,但是只能继承一个抽象类,接口可以继承多个接口Java 接口中声明的变量默认都是 public static final 的。抽象类可以包含非 final 的变量。在JDK1.8之前,接口中不能有静态方法,抽象类中可以有普通方法和静态方法;在 JDK1.8后,接口中可以有默认方法和静态方法,并且有方法体。抽象类可以有构造方法,但是不能直接被 new 关键字实例化。在 JDK1.8 前,抽象类的抽象方法默认访问权限为 protected,1.8默认访问权限为 default,共有 default,protected 、 public 三种修饰符,非抽象方法可以使用四种修饰符;在 JDK1.8 前,接口方法默认为 public,1.8时默认为 public,此时可以使用 public 和 default,1.9时接口方法还支持 private。

12,简述下HashMap

HashMap底层采用了数组+链表的数据结构,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

如果定位到的数组位置不含链表,那么执行查找、添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值initialCapacity默认为16,loadFactory默认为0.75。

public HashMap(int initialCapacity, float loadFactor) {      //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1

悲观锁、乐观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

这里说到了CAS算法,那么什么是CAS算法呢?

CAS算法

一个线程失败或挂起并不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不使用锁的情况下实现多线程安全,因此是一种无锁算法。

CAS算法的定义:CAS的主要作用是不使用加锁就可以实现线程安全,CAS 算法又称为比较交换算法,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

原子更新的基本操作包括:

AtomicBoolean:原子更新布尔变量;AtomicInteger:原子更新整型变量;AtomicLong:原子更新长整型变量;

以AtomicInteger为例,代码如下:

public class AtomicInteger extends Number implements java.io.Serializable { //返回当前的值 public final int get() { return value; } //原子更新为新值并返回旧值 public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } //最终会设置成新值 public final void lazySet(int newValue) { unsafe.putOrderedInt(this, valueOffset, newValue); } //如果输入的值等于预期值,则以原子方式更新为新值 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //原子自增 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //原子方式将当前值与输入值相加并返回结果 public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } }

再如,下面是使用多线程对一个int值进行自增操作的代码,如下所示。

public class AtomicIntegerDemo { private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args){ for (int i = 0; i

如有参考学习的可以直接去我GitHub地址中:https://github.com/733gh/Android-T3参考查阅,望这些干货能够帮助到大家!!!

收藏

上一篇Java 面试题基础

下一篇jvm面试题

  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜