Android 面试面试Android 面试专辑

Android面试基础系列二——Java

2017-10-28  本文已影响112人  thinkChao

此系列文章是我在毕业求职期间,对Android面试相关的基础知识做的一个整理,内容还比较全面,现在将其发布出来,希望对即将求职的同学能有帮助。
这一系列的文章都是使用MarkDown编辑的,源文件也一并公布出来,大家可以在我文章的基础上,根据自己的情况修改或增加内容,定制自己的面试笔记。

链接:http://pan.baidu.com/s/1mhM4RSO 密码:hgzt

一、Java I/O

1、Socket的创建过程

客户端

1、创建Socket对象

Socket socket = new Socket("192.168.5.6",5000);

2、建立连接后,通过输出流向服务端发送请求消息

PrintWriter printWriter = new PrintWriter(socket.getOutputStream(),true);

printWriter.println("hello");

3、通过输入流获取服务端响应的信息

BufferReader bufferReader = new BufferReader(new InputStreamReader(socket.getInputStream()));

String s = bufferReader.readLine();

4、关闭相关资源

服务端

1、创建ServerSocket对象

ServerSocket serverSocket = new ServerSocket(5000);

2、通过accept()方法监听客户端请求

Socket soxket = serverSocket.accept();

3、建立连接后,通过输入流读取客户端的请求消息

BufferReader bufferReader = new BufferReader(new InputStreamReader(socket.getInputStream()));

String s = bufferReader.readLine();

4、通过输出流向客户端发送消息

PrintWriter printWriter = new PrintWriter(socket.getOutputStream(),true);

printWriter.println("hello");

5、关闭相关资源

2、BIO(阻塞)与NIO(非阻塞)

传统的socket数据传输方式,如果客户端还没有对服务器端发起链接请求,那么accept就会阻塞,如果链接成功,数据还没有准备好,也会阻塞。

所以当处理多个链接时,就要有多个线程,由于每个线程都拥有自己的栈空间,而且由于阻塞会导致大量的上下文切换。

BIO

通信模型

缺点

NIO


1、采用了反应器设计模式,和观察者模式很像,只不过观察者模式只能处理一个事件源,而反应器模式可以处理多个事件源。

2、采用双向通道(channel)进行数据传输,而不是传统的单向的流,在通道上我们可以注册我们感兴趣的事件。

3、如图,客户端和服务端各自维护了一个管理通道的对象,叫做Selector,多个channel上的事件都能被它监听到,

我们以服务端为例:如果服务端的Selector上注册了一个读事件,在某个时刻,客户端给服务端发送了一些数据,如果是BIO,它会调用read()方法,阻塞的读取数据,而NIO,会在Selector中添加一个读事件,服务端的处理线程会轮询的访问Selector,如果发现有感兴趣的事件到达,就去处理,如果没有,就会一直阻塞,直到感兴趣的事件到达为止。 这种方式,不用进行线程的切换。

优点

NIO在网络编程中有非常重要的作用,与传统的Socket相比,效率要高处很多。

二、多线程

1、Thread与Runnable

共同点

都要调用Thread产生线程,然后调用start()方法开启线程

区别

1、通过使用Runnable接口,弥补java单继承的缺陷,比Thread更灵活。

2、使用Thread,就要产生相应多个Thread线程;使用Runnable接口,只需要建立一个实现Thread的实例,用这一个实例实现多个线程,就实现了资源的共享。

start()与run()

start()方法是开启线程的方法,真正实现了多线程的运行,但是处于就绪状态,并没有运行。run()方法是线程体,包含要执行的内容,run()方法结束线程也就终止了。如果仅仅单纯的调用run()方法的话,它只会作为一个普通方法来调用。

2、线程间通信

sychronized对象锁

虽然sychronized是为了锁住某个方法,不让它同时调用,但这个方法一定是属于某个对象,所以其实是锁住了某个对象,所以叫对象锁。

sychronized实现线程间通信

即使两个线程调用的同一个对象的不同方法,但是因为sychronized是对象锁,锁住的是对象,所以同一时间只有一个线程在执行。

sychronized与volatile

volatile关键字,被volatile修饰的变量,系统每次用到它是都是直接从内存中读取,而不是缓存,这样,所有的线程在任何时候得到的变量的值都是相同的。其实,也就是不允许将这个变量从主内存中拷贝到自己的线程空间中。

它保证了线程之间的可见性,但不保证操作的原子性,所以仍然存在线程安全问题。

区别:volatile本质上是,会告诉当前的Java虚拟机,当前的变量的值是不确定的,你如果想获取当前的变量值,请从主内存当中获取;sychronized会锁住变量,只有当前线程可以访问,结束后其它的线程也会更新这个变量的值。

其次volatile只能修饰变量。

sychronized与lock

考的不多。
区别:

1、用法上:sychronized在需要同步的对象中加上,可以加在方法上,也可以加在代码块上。而lock,需要指定起始位置和终止位置,lock和unlock。

2、机制上:sychronized托管给java虚拟机执行,lock是 Java代码自己写的控制代码,所以sychronized相对低效。sychronized是CPU悲观锁机制,其它线程只能依靠阻塞来等待线程释放锁;lock是乐观锁机制,其它线程假设没有冲突,如果有冲突就去重试,直到成功为止。

3、性能上:当竞争不是很激烈时,sychronized效率更高;当竞争激烈时,lock效率更高。

sleep()与wait()

都是让线程暂停执行一段时间的方法。

1、原理不同。sleep()方法是Thread类的静态方法,是线程用来控制自身流程的。比如线程执行报时功能,每隔一秒打印出一个时间,那么就可以加一个sleep方法,没隔一秒执行一次。

wait()是Object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其它线程调用notify()方法醒来,也可以设置时间,自己醒来。

2、对锁的处理机制不同。sleep()阻塞线程的执行,但不会释放锁的持有,而wait()会释放它持有的锁。

3、使用区域不同。sleep()可以在任何地方使用,wait()必须在同步代码块中调用。

sleep()与yield()

1、该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级或更高优先级的线程有执行的机会。

2、sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()方法不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行。

wait()/notify()机制

wait()方法是定义在Object大类当中的,它需要在同步代码块中调用,调用之后,它会释放锁,并进入锁对象的等待队列。它需要其它线程调用notify()方法,释放锁之后,它才能重新去竞争这个锁。

notify()仅唤醒一个线程(等待队列中的第一个),notifyAll()唤醒所有线程去竞争。

3、线程相关

如何终止线程的执行

可以使用stop()或suspend()方法来终止线程的执行,但是它俩都有问题。

stop():因为它在终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致,我们还是拿银行转账作为例子,我们还是从A账户向B账户转账500元,我们之前讨论过,这一过程分为三步,第一步是从A账户中减去500元,假如到这时线程就被stop了,那么这个线程就会释放它所取得锁,然后其他的线程继续执行,这样A账户就莫名其妙的少了500元而B账户也没有收到钱。这就是stop方法的不安全性。

suspend():suspend被弃用的原因是因为它会造成死锁。suspend方法和stop方法不一样,它不会强制释放锁,相反它会一直保持对锁的占有,一直到其他的线程调用resume方法,它才能继续向下执行。假如有A,B两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在获得相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时A,B两个线程都不能继续向下执行而形成了死锁。这就是suspend被弃用的原因。

正确姿势:

我们可以采用设置一个条件变量的方式,run方法中的while循环会不断的检测flag的值,在想要结束线程的地方将flag的值设置为false就可以啦!注意这里要将flag设置成volitale的,因为volitale可以保证数据的有效性,如果不设置话,可能会造成子线程多执行一次的错误,例如子线程将flag读到自己线程栈中,flag的值为true,此时子线程的交出执行权,操作系统将执行权交给了主线程,主线程执行flag=false;的操作,希望子线程不要再执行了,但是这一改变子线程是不能看到的,所以子线程还会再向下执行一次,然后重新读取flag的值的时候才会终止。

子线程结束,如何通知主线程

使用Callable和Future配合完成。

https://thinkchao.github.io/2017/07/23/android-11/

主线程如何等待子线程完成任务

1、使用Thread的join()方法

2、使用ExecutorService线程池的awaitTermination()方法。或者通过调用isTerminated()来轮询ExecutorService是否已经终止。通常在调用awaitTermination()之后会立即调用shutdown(),从而产生同步地关闭ExecutorService的效果。

4、线程池

好处

1、重复利用已经创建的线程,降低资源消耗。

2、不需要等待线程创建,提高响应速度。

3、提高线程的可管理性。

ThreadPoolExecutor

ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler) 

1、创建线程池

2、提交任务

但是,线程池提交任务后,没有返回值,我们无法判断线程是否执行成功。

execute()与submit()

需要结果调用submit(),不需要结果调用execute()。

工作流程

1、首先判断基本线程池是否已满

2、判断工作队列是否已满

3、判断整个线程池是否已满

三、高级技术——注解、反射、泛型

参考我的文章

1、注解

概念、作用

用一个词就可以描述注解,那就是元数据,即一种描述数据的数据。

注解不会影响程序代码的执行,无论增加、删除注解,代码都始终如一的执行。

分类

元数据

1、元数据以标签的形式存在于Java代码中

2、元数据描述的信息是类型安全的

3、元数据需要编译器之外的工具,额外的处理,用来生成其它的程序部件

4、元数据可以只存在于Java源代码级别,也可以存在于编译之后的Class文件内部

Android Support Annotation

2、反射

编译时 vs 运行时

Anaimal animal = new Dog();

使用方法

Android中的反射

比如使用隐藏的API。

3、泛型

四、异常处理

1、异常体系

1、Error/Exception

Error是程序无法处理的错误,属于JVM层次的错误,当这种错误发生时,JVM选择终止这些线程;Exception是程序可以处理的错误,它们拥有共同的父类Throwable。

2、运行时异常/检查异常

检查异常发生在编译阶段,编译器强制去捕获此类异常,即把可能出现错误的地方放在tyy代码块中。

运行时异常由JVM进行处理,常见的有:NullPointerException、ClassCastException、ArrayIndexoutOfBoundsExceptoin ……

2、异常处理机制

3、处理原理

1、Java虚拟机用方法调用栈来跟踪每个线程中一系列方法调用过程。

2、如果在执行方法的过程中抛出异常,则Java虚拟机必须找到能捕获该异常的catch代码块

3、当Java虚拟机追溯到调用栈的底部方法时,如果仍然没有找到处理该异常的代码块,则:

首先打印异常信息,然后判断是主线程还是子线程,线程终止,如果是主程序,则程序终止。

4、finally的四种场景

1、finally不被执行的唯一情况是,先执行了System.exit(0),因为这个方法是关闭虚拟机

2、如果在try中执行return 0,用于退出本方法,finally会在return之前执行

3、如果return a,finally代码块不能通过重新给变量赋值的方式改变返回值

4、不要在finally中使用return语句,因为它会引起两种问题:首先,导致数据的不安全;其次,会产生丢失异常,导致catch代码不能被执行

finally、final、finalize

finalize:是一个方法名,由垃圾收集器,在确定一个对象不会再使用时,进行收集,在开发过程中是用不到的。

五、JVM

1、JVM内存模型

Java的内存分配策略有三个:静态分配、堆分配、栈分配,分别对应不同的分配空间。

关于引用:在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

我们知道,计数法和可达性分析法都和引用有关,一个对象只有被引用和没被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。所以引用有了如下分类:

2、垃圾回收机制

where

堆区和方法区。其它三个都是随着线程的创建和销毁而进行的。

when

就是判断对象是否已死,那些死去的就是不能再被任何途径所使用。
1、引用计数法(不用)

每当有一个对象引用它,计数器的值就加一,引用失效就减一,任何时刻计数器为0就是不会再被使用。

很多虚拟机里面并没有选用它,是因为它很难解决相互引用的问题。

2、可达性分析算法

GC使用有向图来记录和管理堆内存中的所有对象,通过这个有向图就可以识别哪些对象是“可达的”,从而回收所有不可达对象。

how

标记-清除法

最基础的回收算法,主要分为标记和清除两个阶段,先统一标记,然后统一回收。之所以说它是最基础的,是因为后续的算法都是基于这种思路,并对其不足进行改进,主要两点不足:

1、效率不高,标记和清除的效率都不高。

2、空间问题,清除之后会产生大量不连续的内存碎片,

复制回收算法

将内存分为大小相等的两块,每次只使用其中的一块,当这一块用完了,就将还存活的对象复制到另一块,然后统一回收内存空间。实现简单,效率高,也没有碎片化问题。

但是代价就是将内存缩小会原来的一半,代价高了点。

标记-整理算法

标记过程和标记-清除一样,但后续并不是直接回收内存,而是让所有的存活的对象都向一端移动,然后直接清理掉端外的内存。

按代回收算法

根据对象存活周期的不同,将Java堆分为老年代和新生代,根据各个代的特点采用不同的算法。

新生代中,每次垃圾回收都有大批对象死去,只有少量存活,就使用复制算法。老年代中因为对象存活时间较长,就使用标记-清楚或标记-整理算法。

引起内存泄漏的根本原因

1、长生命周期对象持有短生命周期对象的引用,就可能引发内存泄漏。

2、资源未关闭

3、相互引用

如何解决内存泄漏的问题

http://www.jianshu.com/p/90caf813682d

如何减少gc出现的次数

3、类加载机制

类的生命周期:加载、验证、准备、解析、初始化、使用、卸载,七个阶段。

加载

1)通过一个类的全限定名来获取此类的二进制字节流

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3)在内存中生成一个代表这个类的java.lang.class 对象,作为方法区这个类的各种数据的访问入口。

验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。

准备(感觉主要处理static)

1) 为类变量(被static修饰的变量)分配内存

2)设置类变量的初始值,比如定义public static value = 123;初始值则设置为0.

解析

该阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

在前面几步中,除了加载阶段用户可以自定义类加载器,其余完全由虚拟机主导和控制。

到了初始化阶段,才真正开始执行类中定义的java程序代码。

4、类加载器

虚拟机设计团队把类加载阶段中的第一步“加载”,也就是通过一个类的全限定名来获取描述此类的二进制字节流这个动作,放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

4、字节码执行引擎

5、其它

1、new的执行过程

《深入理解Java虚拟机》

六、集合类

使用:http://blog.csdn.net/ghost_programmer/article/details/43057045

1、List

按对象的进入顺序保存对象,主要有三个实现类:

ArrayList

不是线程安全的,所以效率较高。

Vector

线程安全,所以比ArrayList效率低。

LinkedList

采用双向链表实现,所以访问效率低,但插入删除效率要比ArrayList和Vector高。

2、Set

集合中的元素不能重复。

TreeSet

实现了SortedSet接口,所以是有序的。

HashSet

不是有序的。

3、Map

HashMap

HashTable

TreeMap

根据键的值进行排序

七、设计模式

参考我的文章

0、六大原则

1、单例模式

代码

自己写过。

使用场景

在确保某个类只有一个实例,避免产生多个对象浪费过多的资源。例如数据库的操作。

2、工厂方法模式

代码

自己写过,注意工厂方法要使用反射来做。

使用场景

工厂方法模式是用来替代“new”一个对象的操作,所以在所有需要生成对象的地方都可以使用。尤其是需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。好处:

1、良好的封装性,当需要创建一个具体的产品对象,只需要知道这个产品的类名,而不需要了解其具体过程,比如构造函数需要哪些参数。

2、屏蔽产品类。调用者只关心接口是否变化,不关心产品类的变化。

3、扩展方便。当增加产品类的时候,除了增加这个产品类,工厂类只需要稍加修改或不修改都可以。

3、观察者模式

之前一直认为,观察者应该是肩负重任的那个角色,会有很多方法,但实际上被观察者才是身负重任的那个,它的方法包括:

而观察者只有一个方法:

4、装饰者模式

使用场景

动态的给一个对象添加一些额外的职责。就增加功能来说,装饰者模式相比生成子类更为灵活。所以,装饰者模式的目的就是:在不需要创造更多子类的情况下,将对象的功能加以扩展。

5、适配器模式

使用场景

1)Adapter通常应用于进行不兼容的类型转换的场景,比如系统需要使用现有的类,而此类的接口不符合系统的需要;

2)还有一种就是输入有无数种情况,但是输出类型是统一的。如果你是搞Android开发,RecyclerView或者ListView中就是这种使用场景。

6、Builder模式

使用场景

1、产品类非常复杂,产品类的调用顺序不同,产生的作用不同,这个时候适合Builder模式。

2、当初始化一个对象特别复杂,如参数多,且很多参数具有默认值的时候。

7、模版方法模式

使用场景

用四个字概括就是流程封装。也就是把某个固定的流程封装到一个final函数中,并且让子类能够定制这个流程中的某些甚至所有的步骤。这就要求父类提取提取共用的代码,提升代码的复用率。

8、策略模式

使用场景

针对同一类型问题的多种处理方式。
可以减少if else的使用,代码看起来更简洁。

9、状态模式

详解看我的博客。

七、Java基础

1、内部类

Static Inner Class 和 Inner Class 的不同:

2、String、StringBuffer

3、Java I/O体系

4、多态的应用场景

5、==、equals、hashcode

==:比较两个变量的值是否相等。如果是基本数据类型,比较的就是值。如果是引用类型,比较的就是在堆内存中的首地址。

equals:Object类提供的方法。 Object中定义的equals方法就是"= =",所以在没有被覆盖的情况下,就是"= ="。但是它可以被覆盖,例如string的equals方法,如果new了两个string对象,一个s1,一个s2,他们是两个对象,在内存中的地址是不同的,所以用‘==’返回的就是false,但是覆盖之后,就可以比较他们里面存储的值。

hashCode(): equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。反过来:hashcode()不等,一定能推出equals()也不等。

在这里hashCode就好比字典里每个字的索引,equals()好比比较的是字典里同一个字下的不同词语。就好像在字典里查“自”这个字下的两个词语“自己”、“自发”,如果用equals()判断查询的词语相等那么就是同一个词语,比如equals()比较的两个词语都是“自己”,那么此时hashCode()方法得到的值也肯定相等;如果用equals()方法比较的是“自己”和“自发”这两个词语,那么得到结果是不想等,但是这两个词都属于“自”这个字下的词语所以在查索引时相同,即:hashCode()相同。如果用equals()比较的是“自己”和“他们”这两个词语的话那么得到的结果也是不同的,此时hashCode() 得到也是不同的。

有人发明了一种哈希算法来提高从集合中查找元素的效率,这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组(使用不同的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希吗就可以确定该对象应该存储在哪个区域…………

http://blog.csdn.net/jiangwei0910410003/article/details/22739953

6、static

作用

1、为该对象分配单一的存储空间

2、被修饰的对象与类,而不是与对象关联在一起,也就是不用创建对象就可以通过类来调用它

7、构造函数

在对象实例化的时候被调用,且只调用一次。

8、什么是回调函数

9、Java引用类型及其使用场景

1、强引用

如果一个对象具有强引用,那垃圾回收器绝不会回收它。

2、软引用

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的

3、弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

弱引用最常见的用处是在集合类中,尤其在哈希表中。哈希表的接口允许使用任何Java对象作为键来使用。当一个键值对被放入到哈希表中之后,哈希表对象本身就有了对这些键和值对象的引用。如果这种引用是强引用的话,那么只要哈希表对象本身还存活,其中所包含的键和值对象是不会被回收的。如果某个存活时间很长的哈希表中包含的键值对很多,最终就有可能消耗掉JVM中全部的内存。

4、虚引用

“虚引用”顾名思义,就是形同虚设,主要用来跟踪对象被垃圾回收器回收的活动。

上一篇 下一篇

猜你喜欢

热点阅读