面试复习——Android工程师之Java面试题

概念相关面试题

1、进程和线程

  1. 地址空间和其他资源:进程间相互独立,进程中包括多个线程,线程间共享进程资源,某进程内的线程在其他进程内不可见
  2. 通信:进程间通信通过IPC机制,线程间通信通过数据段(如:全局变量)的读写,需要进程同步和互斥手段的辅助,以保证数据的一致性
  3. 调度和切换:进程是资源分配单位,线程是cpu调度单位,跟cpu真正打交道的是线程,线程上下文切换比进程上下文切换要快得多

2、内存溢出和内存泄漏

  • 内存溢出:指程序在申请内存时,没有足够的空间供其使用
  • 内存泄漏:指程序分配出去的内存不再使用,无法进行回收

3、面向对象和面向过程

1、面向过程

  1. 优点:性能比面向对象高,因为类的调用需要实例化,开销比较大
  2. 缺点:没有面向对象的易维护、易复用、易拓展

2、面向对象

  1. 优点:易维护、易复用、易拓展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的程序
  2. 缺点:性能比面向过程低

4、Java的四个基本特性

  1. 抽象:把现实生活某一类东西提取出来,成为该类东西的共有特性。抽象一般分为数据抽象和过程抽象,数据抽象是对象的属性,过程抽象是对象的行为特征
  2. 封装:把客观事物进行封装成抽象类,该类的数据和方法只让可信的类操作,对不可信的类隐藏。封装分为属性的封装和方法的封装
  3. 继承:从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力
  4. 多态:同一个行为具有多个不同表现形式或形态的能力,多态的前提是类与类之间必须存在关系,要么继承,要么实现

5、抽象类和接口的区别

  1. 抽象类和接口分别给出了不同的语法定义(包括方法体、成员变量修饰符、方法修饰符)
  2. 抽象是对类的抽象,接口是对行为的抽象
  3. 抽象所体现的是继承关系,是一种"is-a"的关系,接口仅仅实现接口定义的契约,是一种"like-a"的关系
  4. 抽象是自底向上抽象的,接口是自顶向下设计出来的

6、自动装箱与拆箱

Java采用了自动装箱和拆箱机制,节省了常用数值的内存开销和创建对象的开销,提高了效率

  1. 装箱:将基本数据类型包装成它们的引用类型
  2. 拆箱:将包装类型转换成基本数据类型

7、序列化与反序列化

对象的序列化:是把对象转换成字节序列的过程
对象的反序列化:是把字节序列恢复为对象的过程

对象序列化的主要用途:

  1. 可以将字节序列永久的保存在硬盘中,通常放在文件中
  2. 可以在网络上传送字节序列
  3. 两个线程在进行远程通信时,彼此可以发送各种类型。发送方需要把这个Java对象转换为字节序列,接收方则需要把字节序列再恢复为Java对象

8、编译和运行

1、编译时和运行时

  • 编译时:将Java文件编译成.class文件的过程,不涉及到内存的分配
  • 运行时:将虚拟机执行.class文件的过程,涉及到内存的分配

2、编译时类型和运行时类型

  • 编译时类型:由声明该变量时使用的类型决定
  • 运行时类型:由实际赋给该变量的对象决定

3、编译时和运行时的动态绑定

  • 在编译时,调用的是声明类型的成员方法
  • 在运行时,调用的是实际类型的成员方法
  • 对于调用引用实例的成员变量,无论是编译还是运行时,均是编译时类型的成员变量

9、GC简述

当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象,通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。但是,为了保证 GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定

10、serialVersionUID简述

对于实现java.io.Serializable接口的实体类来说,往往都会手动声明serialVersionUID,因为只要你实现了序列化,java自己就会默认给实体类加上一个serialVersionUID。java默认添加的serialVersionUID是会根据实体类的成员(成员变量,成员方法)变化而变化。当我们把实体类序列化到本地后,如果实体类的成员发生了变化,默认添加的serialVersionUID就会发生变化。此时硬盘上序列化对象的serialVersionUID与实体类中的serialVersionUID对不上,就会反序列化失败爆出异常。所以,通常对于实现了SerialVersionUID接口的实体类来说,都会手动声明serialVersionUID

11、Java中4种引用类型

  • StrongReference(强引用):从不回收,对象一直存在,当JVM停止的时候才被终止
  • SoftReference(软引用):可以和引用队列(ReferenceQueue)联合使用,当内存不足时被终止
  • WeakReference(弱引用):可以和引用队列(ReferenceQueue)联合使用,当内存不足时,触发GC后被终止
  • PhantomReference(虚引用):必须和引用队列(ReferenceQueue)联合使用,随时会被回收,触发GC后被终止

SoftReference和WeakReference的区别:

  • WeakReference的生命周期比SoftReference的生命周期短
  • SoftReference多用于图片的缓存,而WeakReference多用于内存泄漏的解决
  • WeakReference可以通过手动GC进行清除

12、线程和守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

Daemon的作用是为其他线程的运行提供便利服务,比如垃圾回收线程就是一个很称职的守护者。User和Daemon两者几乎没有区别,唯一的不同之处就在于,如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有可以守护的对象了,也就没有继续运行程序的必要了

字符串相关面试题

1、String、StringBuffer和StringBuilder

1、可变性

  1. String类是用字符数组保存字符串,即private final char value[],所以String对象不可变
  2. StringBuffer类与StringBuilder类都是继承AbstractStringBuilder类,AbstractStringBuilder是用字符数组保存字符串,即char value[],所以StringBuffer与StringBuilder对象是可变的

2、线程安全性

  1. String类对象不可变,所以理解为常量,线程安全
  2. StringBuffer类对方法加了同步锁,线程安全
  3. StringBuilder类对方法未加同步锁,线程不安全

3、性能

  1. String类进行改变的时候,都会产生新的String对象,然后将指针指向新的String对象
  2. StringBuffer进行改变的时候,都会复用自身对象,性能比String高
  3. StringBuilder行改变的时候,都会复用自身对象,相比StringBuffer能获得10%~15%左右的性能提升,但是得承担多线程的不安全的风险

2、构造器Constructor是否可被override

  1. 构造器不能被重写
  2. 构造器不能被static修饰
  3. 构造器只能用private、protected、public修饰
  4. 构造器不能有返回值

3、String类是否可以继承

String类由final类,故不可以继承,凡是由final修饰的类都不能继承

集合相关面试题

可查看我的博客,按顺序阅读,需要您具备数据结构基础,且基于JDK1.7

1、TreeMap和HashMap

TreeMap HashMap
有序的 无序的
不允许null 允许null
实现Comparable接口 不实现Comparable接口
底层是红黑二叉树,线程不同步 底层是哈希表,线程不同步

2、TreeSet和HashSet

TreeSet HashSet
有序的 无序的
不允许null 允许null
底层是红黑二叉树,线程不同步 底层是哈希表,线程不同步
TreeSet是通过TreeMap实现的,用的只是Map的key HashSet是通过HashMap实现的,用的只是Map的key

3、Collection和Collections

  1. Collection:是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法
  2. Collections:是一个集合的工具类,它包含有各种有关集合操作的静态方法

4、hashCode和equals

equals相等,hashCode必相等;hashCode相等,equals不一定相等

错误相关面试题

1、Error和Exception

Error类和Exception类的父类都是throwable类

  1. Error:一般指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止
  2. Exception:表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常

2、Unchecked Exception和Checked Exception

1、Unchecked Exception

  1. 指的是不可被控制的异常,或称运行时异常,主要体现在程序的瑕疵或逻辑错误,并且在运行时无法恢复
  2. 包括Error与RuntimeException及其子类,如:OutOfMemoryError,IllegalArgumentException, NullPointerException,IllegalStateException,IndexOutOfBoundsException等
  3. 语法上不需要声明抛出异常也可以编译通过

2、Checked Exception

  1. 指的是可被控制的异常,或称非运行时异常
  2. 除了Error和RuntimeException及其子类之外,如:ClassNotFoundException, NamingException, ServletException, SQLException, IOException等
  3. 需要try catch处理或throws声明抛出异常

数据相关面试题

1、xml解析方式

  • Dom解析:将XML文件的所有内容读取到内存中(内存的消耗比较大),然后允许您使用DOM API遍历XML树、检索所需的数据
  • Sax解析:Sax是一个解析速度快并且占用内存少的xml解析器,Sax解析XML文件采用的是事件驱动,它并不需要解析完整个文档,而是按内容顺序解析文档的过程
  • Pull解析:Pull解析器的运行方式与 Sax 解析器相似。它提供了类似的事件,可以使用一个switch对感兴趣的事件进行处理

详细可见我的博客:Android基础——XML数据的三种解析方式

线程相关面试题

1、线程实现的方式

继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的线程

2、如何停止一个线程

  1. 创建一个标识(flag),当线程完成你所需要的工作后,可以将标识设置为退出标识
  2. 使用Thread的stop()方法,这种方法可以强行停止线程,不过已经过期了,因为其在停止的时候可能会导致数据的紊乱
  3. 使用Thread的interrupt()方法和Thread的interrupted()方法,两者配合break退出循环,或者return来停止线程,有点类似标识(flag)
  4. (推荐)当我们想要停止线程的时候,可以使用try-catch语句,在try-catch语句中抛出异常,强行停止线程进入catch语句,这种方法可以将错误向上抛,使线程停止事件得以传播

3、线程的状态转换

面试复习——Android工程师之Java面试题

1、新建(new):创建了一个线程对象
2、可运行(runnable):线程对象创建后,线程调用start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权
3、运行(running):可运行状态(runnable)的线程获得了cpu使用权,执行程序代码
4、阻塞(block):线程因为某种原因放弃了cpu使用权,即让出了cpu使用权,暂时停止运行,直到线程进入可运行(runnable)状态,才有机会再次获得cpu使用权转到运行(running)状态。阻塞的情况分三种:

  • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中
  • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中
  • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态

5、死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期,且死亡的线程不可再次复生

4、什么是线程安全

在多线程访问同一代码的时候,不会出现不确定的结果

5、如何保证线程安全

  • 对非安全的代码进行加锁操作
  • 使用线程安全的类
  • 不要跨线程访问共享变量
  • 使用final类型作为共享变量
  • 对共享变量加锁操作

6、Synchronized如何使用

Synchronized是Java的关键字,是一种同步锁,它可以修饰的对象有以下几种

  1. 修饰代码块:该代码块被称为同步代码块,作用的主要对象是调用这个代码块的对象
  2. 修饰方法:该方法称为同步方法,作用的主要对象是调用这个方法的对象
  3. 修饰静态方法:作用范围为整个静态方法,作用的主要对象为这个类的所有对象
  4. 修饰类:作用范围为Synchronized后面括号括起来的部分,作用的主要对象为这个类的所有对象

7、Synchronized和Lock的区别

相同点:Lock能完成Synchronized所实现的所有功能
不同点:

  1. Synchronized是基于JVM的同步锁,JVM会帮我们自动释放锁。Lock是通过代码实现的,Lock要求我们手工释放,必须在finally语句中释放。
  2. Lock锁的范围有局限性、块范围。Synchronized可以锁块、对象、类
  3. Lock功能比Synchronized强大,可以通过tryLock方法在非阻塞线程的情况下拿到锁

8、多线程的等待唤醒主要方法

  1. void notify():唤醒在此对象监视器上等待的单个线程
  2. void notifyAll():唤醒在此对象监视器上等待的所有线程
  3. void wait():导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法
  4. void wait(long timeout):导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量
  5. void wait(long timeout, int nanos):导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量

9、sleep和wait的区别

sleep() wait()
是Thread类的方法 是Object类中的方法
调用sleep(),在指定的时间里,暂停程序的执行,让出CPU给其他线程,当超过时间的限制后,又重新恢复到运行状态,在这个过程中,线程不会释放对象锁 调用wait()时,线程会释放对象锁,进入此对象的等待锁池中,只有此对象调用notify()时,线程进入运行状态

10、多线程中的死锁

死锁:指两个或两个以上的线程在执行的过程中,因抢夺资源而造成互相等待,导致线程无法进行下去

产生死锁的4个必要条件

  1. 循环等待:线程中必须有循环等待
  2. 不可剥夺:线程已获得资源,再未使用完成之前,不可被剥夺抢占
  3. 资源独占:线程在某一时间内独占资源
  4. 申请保持:线程因申请资源而阻塞,对已获得的资源保持不放

11、什么叫守护线程,用什么方法实现守护线程

守护线程:指为其他线程的运行提供服务的线程,可通过setDaemon(boolean on)方法设置线程的Daemon模式,true为守护模式,false为用户模式

12、Java中的BIO,NIO,AIO分别是什么,应用场景是什么

  • BIO:同步并阻塞,服务器实现模式是一个连接对应一个线程,即客户端有连接请求时,服务器就会开启一个线程进行处理,如果这个连接不做任何事情时,会造成不必要的线程开销,可以使用线程池进行改善。其应用场景适用于连接数目比较小且固定的架构,这种方式对服务器资源要求较高,对线程并发有局限性
  • NIO:同步非阻塞,服务器实现模式是一个请求对应一个线程,即客户端的连接请求都会注册在多路复用器上,当多路复用器轮询到有I/O请求时才启动一个线程进行处理。其应用场景适用于连接数目多且连接短的架构,对线程并发有局限性
  • AIO:异步非阻塞,服务器实现模式是一个有效请求对应一个线程,即客户端的I/O请求完成之后,再通知服务器去启动一个线程进行处理。其应用场景适用于连接数目多且连接长的架构,充分体现出并发性

13、Java中的IO和NIO的区别

  1. IO是面向流的,NIO是面向缓冲区的
  2. IO的各种流是阻塞的,NIO是非阻塞模式

14、volatile关键字

用volatile修饰的变量,线程在每次修改变量的时候,都会读取变量修改后的值,可以简单的理解为volatile修饰的变量保存的是变量的地址。volatile变量具有synchronized的可见性,但是不具备原子性

  • 可见性:在多线程并发的条件下,对于变量的修改,其他线程中能获取到修改后的值
  • 原子性:在多线程并发的条件下,对于变量的操作是线程安全的,不会受到其他线程的干扰

volatile不是线程安全的,要使volatile变量提供理想的线程安全,必须同时满足下面两个条件

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中

比如增量操作(x++)看上去类似一个单独操作,实际上它是一个由[读取-修改-写入]操作序列组成的组合操作,必须以原子方式执行,而volatile不能提供必须的原子特性。实现正确的操作,应该使x的值在操作期间保持线程安全,而volatile变量无法实现这点

然而,Java提供了java.util.concurrent.atomic.*包下的变量或引用,让变量或对象的操作具有原子性,在高并发的情况下,依然能保持获取到最新修改的值,常见的有AtomicBoolean、AtomicReference等

  • volatile原理:对于值的操作,会立即更新到主存中,当其他线程获取最新值时会从主存中获取
  • atomic原理:对于值的操作,是基于底层硬件处理器提供的原子指令,保证并发时线程的安全

反射相关面试题

1、什么是反射

在运行状态中,对于任意一个类,都可以获得这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性

2、反射使用步骤

  1. 获取类的字节码(getClass()、forName()、类名.class)
  2. 根据类的方法名或变量名,获取类的方法或变量
  3. 执行类的方法或使用变量,如果不使用,也可以创建该类的实例对象(通过获取构造函数执行newInstance方法)

进程相关面试题

1、进程的优先级

优先级从低到高排列,优先级最高的进程,最先获取资源,最后释放

1、空进程

这是Android系统优先杀死的,因为此时该进程已经没有任何用途

2、后台进程

包含不可见的Activity,即跳转到其他Activity后,由于资源不足,系统会将原来的Activity杀死(即跳转的来源)

3、服务进程

即Service,当系统资源不足时,系统可能会杀掉正在执行任务的Service,因此在Service执行比较耗时的操作,并不能保证一定能执行完毕

4、可见进程

当前屏幕上可以看到的Activity,例如显示一个对话框的Activity,那么对话框变成了前台进程,而调用他的Activity是可见进程,但并不是前台的

5、前台进程

当前处于最前端的Activity,也就是Android最后考虑杀死的对象,一般来说,前台进程Android系统是不会杀死的,只有当前4个都杀掉资源依旧不够才可能会发生

2、IPC机制

面试复习——Android工程师之Java面试题

类加载器相关面试题

1、什么是类加载器

ClassLoader是用来动态加载class文件到内存中

2、类加载器类型

  • App ClassLoader(应用类加载器或系统加载器):这个加载器使用java实现,使用广泛,负责加载classPath中指定的类。具体的使用场合,在加载classPath中指定的而扩展类加载器没有加载的类,若扩展类加载器加载了classPath中的类,则系统类加载器则没有机会加载。用户定义的类一般都是系统类加载器加载的。可以通过:ClassLoader.getSystemClassLoader()获得
  • Extension ClassLoader(扩展类加载器):它负责加载Java的标准扩展,一般使用Java实现的,负责加载jre/lib/ext中的类。和普通的类加载器一样。可以通过:ClassLoader.getSystemClassLoader().getParent()获得
  • BootStrap ClassLoader(引导类加载器):纯C++实现的加载器,没有对应的Java类,它负责加载jdk中jre/lib目录下的系统核心库。对于java程序无法获得它,像上文中获得扩展类加载器的父类加载器是null。像String,Integer,Double类都是由引导类加载器加载的

3、双亲委托模型(父委托加载机制)

当加载一个类时,首先会判断当前类是否已经被加载,如果被加载直接返回当前类加载器,如果没有被加载,则把机会让给父类,先让父类加载,若是父类中不能加载,则会去找到Bootstrap加载器,如果Bootstrap加载器加载失败,则会退回上层,自己通过findClass自己去加载对应的路径(这是孝顺型的,先想到父类,但是他们不是通过继承来实现的)

面试复习——Android工程师之Java面试题

具体实现代码在ClassLoader类中的loadClass方法中,API19和API24加载过程有少许区别(以下是API24的源码),但大部分是一致的,下面执行的逻辑与我们上面所说的一致

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    //首先会判断当前类是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {
                //把机会让给父类,先让父类加载
                c = parent.loadClass(name, false);
            } else {
                //如果父类无法加载,则会去找Bootstrap加载器
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
        }

        if (c == null) {
            long t1 = System.nanoTime();
            //如果前面的都加载失败,则调用findClass用自己的加载器加载
            c = findClass(name);
        }
    }
    return c;
}

优点:

  • 父委托机制会先去加载系统自带的class,而不会去加载我们自定义的高仿的系统class(比如ArrayList),可以保证我们的程序的安全而不会被恶意注入

4、类加载过程

面试复习——Android工程师之Java面试题

  • 加载:将类的信息从文件中获取并且载入到JVM内存中
  • 验证:检查读入的结构是否符合JVM规范的描述
  • 准备:分配一个结构用来存储类信息
  • 解析:把这个类的常量池的所有符号引用改变成直接引用
  • 初始化:执行静态初始化程序、类构造器方法的过程

5、自定义类加载器

public class MyClassLoader extends DexClassLoader {

    public MyClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData != null) {
            return defineClass(name, classData, 0, classData.length);
        } else {
            throw new ClassNotFoundException();
        }
    }

    /**
     * 读取文件的字节数组
     *
     * @param name
     * @return
     */
    private byte[] getClassData(String name) {
        try {
            InputStream inputStream = new FileInputStream(name);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int buffSize = 4096;
            byte[] buffer = new byte[buffSize];
            int bytesRead = -1;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

6、类加载主要方法的区别

  • findClass():查找指定路径下的class文件
  • loadClass():加载class字节码文件
  • defineClass():将字节数组流转换成字节码

易错题

1、静态代码块、构造代码块、构造方法的执行顺序

class Fu{
    static {
        System.out.println("父静态代码块");
    }
    {
        System.out.println("父代码块");
    }
    public Fu(){
        System.out.println("父构造函数");
    }
}

class Zi extends Fu{
    static {
        System.out.println("子静态代码块");
    }
    {
        System.out.println("子代码块");
    }
    public Zi(){
        System.out.println("子构造函数");
    }
}

public class Text{
    public static void main(String[] args) {
        Zi zi = new Zi();
    }
}

输出结果

父静态代码块
子静态代码块
父代码块
父构造函数
子代码块
子构造函数

版权声明:程序员胖胖胖虎阿 发表于 2022年9月12日 上午4:08。
转载请注明:面试复习——Android工程师之Java面试题 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...