整理了Android面中常见的62个Java知识点...
Posted Pandafz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了整理了Android面中常见的62个Java知识点...相关的知识,希望对你有一定的参考价值。
这1个月有点忙,面试了10多个小厂和2、3个大厂,给我的感觉就是基础不牢,地动山摇。一般的面试的逻辑就是面向简历,深挖细节。字节的一面问了我一个半小时,反思一下,真的基础非常的重要,一些中小厂可能会额外提到行业看法面、个人世界观面等,本文主要还是针对专业技术面更多。
话不多说,基于我现在被问到的一些情况,也查看了全网诸多的面试题,我总结了一些在android面中的Java题。
由于本人见识非常有限,写的博客难免有错误或者疏忽的地方,希望各位人才们指点一二。
努力不辜,时光不负。继续冲冲冲!
一、Java概述
1.JVM、JRE和JDK的关系
- JVM:Java虚拟机,Java程序需要运行在虚拟机
- JRE:Java虚拟机+Java程序所需的核心类库
- JDK:Java虚拟机+Java程序所需的核心类库(JRE)+Java开发工具包
2.谈谈你对类生命周期的认识?
jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周
期中扮演着比较重要的角色:
- 方法区: 在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
- 常量池: 常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
- 堆区: 用于存放类的对象实例。
- 栈区: 也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段
3.谈谈你对面向对象和面向过程的理解
面向过程:分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现
面向对象:把构成问题事务分解成各个对象,然后描述对象的行为
举个例子:下五子棋:
- 面向过程的设计思路:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。
- 面向对象的设计思路:1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定
4.面向过程和面向对象的优缺点
面向过程:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象:
- 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
- 缺点:没有面向对象易维护、易复用、易扩展
举个例子:
面向过程的程序是一份蛋炒饭,面向对象的程序是一份盖浇饭。
蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是重新做一份青菜炒饭了。盖浇饭更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。
到底是蛋炒饭好还是盖浇饭好呢?其实这类问题都很难回答,非要比个上下高低的话,就必须设定一个场景,否则只能说是各有所长。如果大家都不是美食家,没那么多讲究,那么从饭馆角度来讲的话,做盖浇饭显然比蛋炒饭更有优势,他可以组合出来任意多的组合,而且不会浪费。
盖浇饭的好处就是"菜"“饭"分离,从而提高了制作盖浇饭的灵活性。饭不满意就换饭,菜不满意换菜。用软件工程的专业术语就是"可维护性"比较好,“饭” 和"菜"的耦合度比较低。蛋炒饭将"蛋”“饭"搅和在一起,想换"蛋”"饭"中任何一种都很困难,耦合度很高,以至于"可维护性"比较差。软件工程追求的目标之一就是可维护性,可维护性主要表现在3个方面:可理解性、可测试性和可修改性。面向对象的好处之一就是显著的改善了软件系统的可维护性。
5.Java和C++的区别
- 都是面向对象的语言,都支持封装、继承和多态
- Java不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,接口可以多继承,C++支持多重继承
- Java有自动内存管理机制,不需要程序员手动释放无用内存
- Java可跨平台运行,C++要关注平台差异性
二、基础语法
6.&和&&的区别
运算符 | 功能描述 | 说明 |
---|---|---|
&& | 短路与 | 都为true才为true,从左到右依次判断,节省计算机资源提高逻辑运算的速度 |
& | 无条件与 | 全部都要判断 |
|| | 短路或 | 全为false才为false,从左到右依次判断,节省计算机资源提高逻辑运算的速度 |
| | 无条件或 | 全部都要判断 |
7. "= =“和equals方法究竟有什么区别?
== 对基本类型和引用类型作用效果是不同的,如下所示:
- 基本类型:比较的是值是否相同;
- 引用类型:比较的是引用是否相同;
equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等
8.关键字final和static是怎么使用的?
static关键字主要有两种作用:
- 第一,为某特定数据类型或对象分配单一的存储空间,而与创建对象的个数无关。
- 第二,实现某个方法或属性与类而不是对象关联在一起
static修饰方法/变量:static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的;根据Java中定义变量位置的不同,变量有两大类:成员变量和局部变量,而成员变量里面根据有无static修饰又可分为类变量和实例变量
- 静态方法中不能直接访问非静态成员方法和非静态成员变量,非静态成员方法可直接可以访问所有成员方法/成员变量
- 同类可直接调用静态方法/类变量,不同类则是类.静态方法/类变量
- 同类非静态方法调用非静态方法/实例变量时,可直接调用方法名()/实例变量名;其它情况下调用非静态方法均要实例化对象通过对象调用非静态方法,类名 对象名 = new 类名(); 对象名.静态方法名()/实例变量名;
- 静态方法中,不能使用this关键字, this是相对于某个对象而言的,static修饰的方法是相对于类的,因此不能在静态方法中用this
static修饰类:
- 实例内部类:直接定义在类当中的一个类,在类前面没有任何一个修饰符。
- 静态内部类:在内部类前面加上一个static。
- 局部内部类:定义在方法当中的内部类,局部类当中不能使用static变量,不能使用 public、protected、private 修饰。
- 匿名内部类:属于局部的一种特殊情况。
final修饰方法/变量/类
- final有不可改变,最终的意思,可以用来修饰非抽象类、成员方法和变量
- 修饰类:该类不能再派生出新的子类,不能作为父类被继承。因此,一个类不能同时被声明为abstract 和 final,抽象类要被引用,所以不能用final修饰
- 修饰方法:该方法不能被子类重写。
- 修饰变量:该变量必须在声明时给定初值,而在以后只能读取,不可修改。 如果变量是对象,则指的是引用不可修改,但是对象的属性还是可以修改的。
9.switch语句后的控制表达式
- JDK1.0 - 1.4 数据类型接受 byte short int char
- JDK1.5 数据类型接受 byte short int char enum(枚举)
- JDK1.7 数据类型接受 byte short int char enum(枚举), String,对应的包装类型
10.成员变量和局部变量的区别
不同点 | 局部变量 | 成员变量 |
---|---|---|
定义位置 | 方法内部 | 方法外部 |
作用范围 | 方法当中 | 整个类 |
默认值 | 手动赋值 | 有默认值 |
内存位置 | 栈内存 | 堆内存 |
生命周期 | 方法进栈而诞生,方法出栈而消失 | 对象创建而诞生,对象被垃圾回收而消失 |
11.lamda表达式了解过?
Lambda的前提条件
- Lambda只能用于替换有且仅有一个抽象方法的接口和匿名内部类对象,这种接口称为函数式接口
- Lambda具有上下文推断的功能,所以我们才会出现Lambda的省略格式
Lambda的使用场景
- 列表迭代:输出列表的每个元素
- 事件监听
- Predicate 接口
- Map 映射
- Reduce 聚合:对多个对象进行过滤并执行相同的处理逻辑
- 代替 Runnable:创建线程
12.正则表达式掌握过吗,在项目里面怎么用到的
正则表达式是一种用来匹配字符串的强有力的武器,用一种描述性的语言定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了
使用场景:
- 数据有效性验证:用户注册模块是应用正则表达式最集中的地方,主要是用于验证用户帐号、密码、EMAIL、电话号码、QQ号码、身份证号码、家庭地址等信息。如果填写的内容与正则表达式不匹配,可以断定填写的内容是不合乎要求或虚假的信息;
- 模糊查询,批量替换:可以在文档中使用一个正则表达式来查找匹配的特定文字,然后可以全部将其删除,或者替换为别的文字。
Android中正则表达式的用法:
- 核心类:
Pattern
:正则表达式的编译后的对象形式,即正则模式: 将一个字符串转成正则匹配模式对象
Matcher
是正则模式匹配给定字符串的匹配器,Pattern对象调用匹配器matcher()方法,查找符合匹配要求的匹配项
13.什么是拆箱 & 装箱,能给我举例子吗?
拆箱:包装类型转换为基本类型
装箱:基本类型转换为包装类型
Integer i1 = 40;Integer i2= 40;
进行比较时,基本类型int
被装箱为包装类型Integer
。在Java中,基本类型比较的是值,而封装类型比较的是对象的地址。但是这两个包装类对象是同一个对象。因为Integer类,里面涉及到缓存机制,如果给定的基本类型int值在-128到127之间的话,就会直接去cache数组里取,如果不在这个范围的话,那么就会创新的对象。
`平常我们在Java中使用的都是HashMap等来保存数据,但是有一个严重的问题就是HashMap里的key以及value都是泛型,那么就会不可避免的遇到装箱和拆箱的过程,而这个过程是很耗时的,所以为了规避装箱拆箱提高效率,于是诞生了SparseArray等集合类,但是SparceArray效率高也是有条件的,它适用于数据量比较小的场景,而在Android开发中,大部分场景都是数据量比较小的,在数据很大的情况下,当然还是hashmap效率较优。
三、实用类库
14.日期时间了解?谈谈做过那些业务?
切换时间格式,增加不同国家时设置功能,用到了TextClock、Calendar、SimpleDateFormat、AlarmManager类。
TextClock中setFormat24Hour方法设置24小时制度/12小时制
Calendar与SimpleDateFormat类,设置当前时间以及当前时间显示的格式
AlarmManager
中:setTimeZone(String timeZone)方法用来设置系统的默认时区。需要android.permission.SET_TIME_ZONE.权限:mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
15.字符串的更改
当切换系统语言时,引用到一些的string的写法会有不同,一般有以下三种方式选择:
.字符串的反转
- charAt():通过String类的charAt()的方法来获取字符串中的每一个字符,然后将其拼接为一个新的字符串
- toCharArray():通过String的toCharArray()方法可以获得字符串中的每一个字符并转换为字符数组,然后用一个空的字符串从后向前一个个的拼接成新的字符串。
- reverse():通过StringBuiler或StringBuffer的reverse()的方法
字符串替换
- replace():替换字符串中所有指定的字符,然后生成一个新的字符串,原来的字符串不发生改变
- replaceAll():字符串中某个指定的字符串替换为其它字符串
replaceFirst():替换第一个出现的指定字符串
ArrayMap的key-value改变字符串
保证key值相同,选择不同系统语言时,改变value值:
if(true)
arrayMap.put(“A”, “GB”);
else
arrayMap.put(“A”, “GA”);
16.String、StringBuilder、StringBuffrer的区别
- String类是不可变类,任何对String的改变都会引发新的String对象的生成;
- StringBuffer是可变类,任何对它所指代的字符串的改变都不会产生新的对象,线程安全的。
- StringBuilder是可变类,线性不安全的,不支持并发操作,不适合多线程中使用,但其在单线程中的性能比StringBuffer高。
四、继承
17.谈一谈对值传递和引用传递的理解
引用类型传递是栈地址的传递,操作任何一个引用变量都会影响到在堆内存中实际对象的值
简单类型传递是具体值传递,如果在被传递函数中改变了这个传进来的值,不会改变原始的值
18.super 和 this 的异同
指代上的区别
- super:是对当前对象中父对象的引用。
- This:指当前对象的参考。
引用对象上的区别
- super:直接父类中引用当前对象的成员(当基本成员和派生类具有相同成员时,用于访问直接父类中隐藏父类中的成员数据或函数定义)。
- This:表示当前对象的名称(程序中容易出现歧义的地方,应该用来表示当前对象;如果函数的成员数据与该类中成员数据的名称相同,应用于表示成员变量名称)。
调用函数上的区别
- super:在基类中调用构造函数(是构造函数中的第一条语句)。
- This:在此类中调用另一个结构化的构造函数(是构造函数中的第一条语句)。
五、多态
19.重载(Overload)和重写(Override)的区别
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:一个类中有多个同名的方法,但是具有有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)。
重写:发生在子类与父类之间,子类对父类的方法进行重写,参数都不能改变,返回值类型可以不相同,但是必须是父类返回值的派生类。即外壳不变,核心重写!重写的好处在于子类可以根据需要,定义特定于自己的行为。
重写 | 重载 | |
---|---|---|
是否同类 | 不能同类 | 可同类也可不同类 |
方法名的参数形式是否相同 | 必须相同 | 必须不同 |
返回类型 | 必须相同 | 可同可不同 |
访问修饰符 | 子类不能比父类权限小 | 无要求 |
方法体异常 | 不能抛出新的异常或异常不能范围变大 | 无要求 |
构造方法 | 不能被重写 | 可以被重载 |
多态实现方式 | 实现运行时多态 | 实现编译时多态 |
20. 接口与抽象类的异同
相似点
- 两者都可包含抽象方法。实现抽象类和接口的非抽象类必须实现这些抽象方法
- 两者都不能用来实例化对象。可以声明抽象类和接口的变量,对抽象类来说,要用抽象类的非抽象子类来实例化该变量;对接口来说,要用实现接口的非抽象子类来实例化该变量
- 两者的子类如果都没有实现抽象类(接口)中声明的所有抽象方法,则该子类就是抽象类
- 两者都可以实现程序的多态性
不同点
- 一个类只能继承一个直接父类,但是可以实现多个接口
- 抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 抽象类可以有构造函数;接口不能有。
- 抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
- 接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
- 抽象类不能在Java 8 的 lambda 表达式中使用
- 接口体现的是一种规范(打印机和相机都有打印的功能),与实现接口的子类中不存在父与子的关系;抽象类与其子类存在父与子的关系(圆形和方形都是一种形状)
六、 异常处理
21. Exception与Error的区别,RuntimeException
Error(错误):通常是灾难性的致命错误,不是程序(程序猿)可以控制的,如内存耗尽、JVM系统错误、堆栈溢出等。应用程序不应该去处理此类错误,且程序员不应该实现任何Error类的子类。
Exception(异常):用户可能捕获的异常情况,可以使用针对性的代码进行处理,如:空指针异常、网络连接中断、数组下标越界等。
RuntimeException类及其子类称为非检查型异常,Java编译器会自动按照异常产生的原因引发相应类型的异常,程序中可以选择捕获处理也可以不处理,虽然Java编译器不会检查运行时异常,但是也可以去进行捕获和抛出处理。RuntimeException类和子类以及Error类都是非受检异常。
22.几种异常类型
Java 的所有异常可以分为受检异常(checked exception)和非受检异常(unchecked exception)。
受检异常
编译器要求必须处理的异常。Exception 中除 RuntimeException 及其子类之外的异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
非受检异常
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException及其子类)和错误(Error)。
运行时异常
定义:RuntimeException 类及其子类。
特点:RuntimeException为Java虚拟机在运行时自动生成的异常,如被零除和非法索引、操作数超过数组范围、打开文件不存在等。此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。
RuntimeException类及其子类称为非检查型异常,Java编译器会自动按照异常产生的原因引发相应类型的异常,程序中可以选择捕获处理也可以不处理,虽然Java编译器不会检查运行时异常,但是也可以去进行捕获和抛出处理。RuntimeException类和子类以及Error类都是非受检异常。
编译时异常
特点: Exception中除RuntimeException及其子类之外的异常,该异常必须手动在代码中添加捕获语句来处理该异常。编译时异常也称为受检异常,一般不进行自定义检查异常。。
23. Java语言如何进行异常处理,关键字:throws、throw、try、catch、finally分别如何使用?
try
语句中存放的是可能发生异常的语句。当异常抛出时,异常处理机制负责搜寻参数与异常类型相匹配的第一个处理程序,然后进入catch语句中执行,此时认为异常得到了处理。如果程序块里面的内容很多,前面的代码抛出了异常,则后面的正常程序将不会执行,系统直接catch捕获异常并且处理
catch
语句可以有多个,用来匹配多个异常,捕获异常的顺序与catch语句的顺序有关,当捕获到对应的异常对象时,剩下的catch语句不再进行匹配,因此在安排catch语句的顺序时,首先应该捕获最特殊的异常,然后一般化。catch的类型是Java语言定义的或者程序员自己定义的,表示抛出异常的类型。异常的变量名表示抛出异常的对象的引用,如果catch捕获并匹配了该异常,那么就可以直接用这个异常变量名来指向所匹配的异常,并且在catch语句中直接引用。部分系统生成的异常在Java运行时自动抛出,也可通过throws关键字声明该方法要抛出的异常,然后在方法内抛出异常对象。
final
语句为异常提供一个统一的出口,一般情况下程序始终都要执行final语句,final在程序中可选。final一般是用来关闭已打开的文件和释放其他系统资源。try-catch-final可以嵌套。
throws
关键字和 throw 关键字在使用上的几点区别如下:
- throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常。
- throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。调用该方法的方法必须包含可处理异常的代码,否则也要在方法声明中用 throws 关键字声明相应的异常。
有4
种特殊情况,finally块不会被执行:
- finally语句块中发生了异常
- 前面的代码中执行了System.exit()退出程序
- 程序中所在的线程死亡
- 关闭CPU
24. 关于return和finally的关系
try中有return
无论在什么位置添加return,finally子句都会被执行
catch和try中都有return
当try中抛出异常且catch中有return语句,finally中没有return语句,java先执行catch中非return语句,再执行finally语句,最后执行return语句。若try中没有抛出异常,则程序不会执行catch体里面的语句,java先执行try中非return语句,再执行finally语句,最后再执行try中的return语句。
finally 中有return
finally中有return时,会覆盖掉try和catch中的return。
finally中没有return语句,但是改变了返回值
如果finally中定义的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块中之前保存的值;如果finally中定义的数据是是引用类型,则finally中的语句会起作用,try中return语句的值就是在finally中改变后该属性的值。
七、输入与输出流
25.java 中 IO 流分为几种?
Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO流的40多个类大部分都是从如下4个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以分为字节流和字符流;
- 按照流的角色划分,可以分为节点流和处理流。
26.BIO,NIO,AIO 有什么区别,如何理解同步和异步,阻塞和非阻塞
同步与异步
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
- 同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
- 阻塞和非阻塞的区别最大在于有没有一直在干其他的活动。
BIO是什么
同步阻塞:举个例子 : 我现在上厕所 现在厕所的坑已经满了 我什么事情都不做 我就一直等(主动观察)哪一个 坑没人了 ,我就立马去占坑 通过这个示例 可以理解这是同步阻塞的IO
NIO是什么
NIO : 同步非阻塞 New IO Non-Block IO 举个例子: 我现在上厕所 现在厕所的坑已经满了 这时候我不会像之前一样 我会出去抽支烟 或者微信摇一摇 然后我会时不时回去厕所主动看看 看看有没有人走 然后再占有坑
AIO是什么
异步非阻塞IO 举个例子: 我没有在厕所里面等着 而是在厕所外面玩手机,如果有人上完厕所他告诉我: 我好了 你去吧, 这时候我再回去厕所做我自己的事情
异步阻塞IO 举个例子: 开发中非常少 我现在上厕所 现在厕所的坑已经满了 这时候比较懒 什么也不做 就在坑旁干等着 等上厕所的人上好了之后告诉我 我好了 你去吧
它们之间的区别
BIO: 发起请求–>一直阻塞–>处理完成
NIO: Selector主动轮询channel–>处理请求–>处理完成
AIO: 发起请求–>通知回调。
八、集合(容器)
27.谈谈Java集合中那些线程安全的集合 & 实现原理
首先要明白线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。
Vector、ArrayList、LinkedList
- Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList,LinkedList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好
在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能,而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList
HashTable,HashMap,HashSet
- Hashtable:基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。- HashMap,HashSet不是线程安全的
- HashMap: 实现了Map接口,Map接口对键值对进行映射。Map中不允许重复的键。Map接口有两个基本的实现,HashMap和TreeMap。TreeMap保存了对象的排列次序,而HashMap则不能。HashMap
允许键和值为null
。HashMap是非synchronized的,但是HashMap可以通过Collections
进行同步:Map m = Collections.synchronizeMap(hashMap);
, 这样多个线程同时访问HashMap时,能保证只有一个线程更改Map。public Object put(Object Key,Object value)方法用来将元素添加到map中- HashSet: 实现了Set接口,不允许集合中有重复的值,对象存储在HashSet之前,要先确保对象重写equals()和hashCode()方法,这样才能比较对象的值是否相等,以确保set中没有储存相等的对象。如果我们没有重写这两个方法,将会使用这个方法的默认实现。public boolean add(Object o)方法用来在Set中添加元素,当元素值重复时则会立即返回false,如果成功添加的话会返回true。
28.HashMap和Hashtable的区别
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。
- HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。
- HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
- 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器,不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
- 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。
- HashMap不能保证随着时间的推移Map中的元素次序是不变的。
29.HashSet和HashMap的区别
- HashMap实现了Map接口, HashSet实现了Set接口
- HashMap储存键值对, HashSet仅仅存储对象
- HashMap使用put()方法将元素放入map中, HashSet使用add()方法将元素放入set中
- HashMap中使用键对象来计算hashcode值, HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
- HashMap比较快,因为是使用唯一的键来获取对象 HashSet较HashMap来说比较慢.
30.TreeMap和TreeSet的区别与联系
相同点:
- TreeMap和TreeSet都是有序的集合(非线程安全的),也就是说他们存储的值都是排好序
的。- TreeMap和TreeSet都是非同步集合,因此他们不能在多线程之间共享,不过可以使用方法Collections.synchroinzedMap()来实现同步
- 运行速度都要比Hash集合慢,他们内部对元素的操作时间复杂度为O(logN),而
HashMap/HashSet则为O(1)。- 要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。
不同点:
- 最主要的区别就是TreeSet和TreeMap分别实现Set和Map接口
- TreeSet只存储一个对象,而TreeMap存储两个对象Key和Value(仅仅key对象有序)
- TreeSet中不能有重复对象,而TreeMap中可以存在
- TreeMap的底层采用红黑树的实现,完成数据有序的插入,排序。因此它要求一定要有Key比较的方法,要么传入Comparator实现,要么key对象实现Comparable接口。
31. ArrayMap和HashMap的区别
HashMap:
内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表),HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢,所以在Android中,HashMap是比较费内存的。
ArrayMap:
是一个<key,value>映射的数据结构,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%。
HashMap和ArrayMap各自的优势数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。
而数据量比较大的时候,则推荐使用HashMap。
- 查找效率
HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降。所以对于数量比较大的情况下,推荐使用HashMap- 扩容数量
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申请4个。这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果当数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。- 扩容效率
HashMap每次扩容的时候时重新计算每个数组成员的位置,然后放到新的位置。
ArrayMap则是直接使用System.arraycopy。所以效率上肯定是ArrayMap更占优势。这里需要说明一下,网上有一种说因为ArrayMap使用System.arraycopy更省内存空间,这一点我真的没有看出来。arraycopy也是把老的数组的对象一个一个的赋给新的数组。当然效率上肯定arraycopy更高,因为是直接调用的c层的代码。- 内存耗费
以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。由于ArrayMap只缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的节省内存的。
32. Collection 和 Collections的区别?
Collection:
是集合类的上层接口。本身是一个Interface,里面包含了一些集合的基本操作。Collection接口时Set接口和List接口的父接口
Collections
Collections是一个集合框架的帮助类,里面包含一些对集合的排序,搜索以及序列化的操作。最根本的是Collections是一个类,Collections 是一个包装类,Collection 表示一组对象,这些对象也称为 collection 的元素。一些collection 允许有重复的元素, 而另一些则不允许,一些 collection 是有序的,而另一些则是无序的。
33. Map的遍历方式有哪些?
在for-each循环中使用entries来遍历
在for-each循环中遍历keys或values。
使用Iterator遍历
通过键找值遍历(效率低)
九、Java并发
34. 什么是线程,什么是进程,两者区别?
进程 :进程是并发执行程序在执行过程中资源分配和管理的基本单位(资源分配的最小单位)。进程可以理解为一个应用程序的执行过程,应用程序一旦执行,就是一个进程。每个进程都有自己独立的地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段。通常来说,应用中的 Activity、Service 等四大组件默认都位于一个进程里面,并且这个进程名称的默认值就是我们给应用定义的包名。系统为每个进程分配的内存是有限的,比如在以前的低端手机上常见是 16M,现在的机器内存更大一些,32M、48M,甚至更高。但是,总是有限的,毕竟一个手机出厂之后RAM 的大小就定了,总是无法满足所有应用的需求。所以,一个明智的选择就是使用多进程,将一些看不见的服务、比较独立而又相当占用内存的功能运行在另外一个进程当中,主动分担主进程的内存消耗。常见如,应用中的推送服务,音乐类App 的后台播放器等等,单独运行在一个进程中。
线程: 程序执行的最小单位。每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。引入目的是为了减少程序在并发执行过程中的开销,使OS的并发效率更高。
进程与线程的区别
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
- 地址空间: 同一进程的所有线程共享本进程的地址空间,而不同的进程之间的地址空间是独立的。
- 资源拥有: 同一进程的所有线程共享本进程的资源,如内存,CPU,IO等。进程之间的资源是独立的,无法共享。
- 执行过程:每一个进程可以说就是一个可执行的应用程序,每一个独立的进程都有一个程序执行的入口,顺序执行序列。但是线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制进行控制。
- 健壮性: 因为同一进程的所以线程共享此线程的资源,因此当一个线程发生崩溃时,此进程也会发生崩溃。 但是各个进程之间的资源是独立的,因此当一个进程崩溃时,不会影响其他进程。因此进程比线程健壮。线程执行开销小,但不利于资源的管理与保护。进程的执行开销大,但可以进行资源的管理与保护。进程可以跨机器前移。
进程与线程的联系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源;
-处理机分给线程,即真正在处理机上运行的是线程;
-线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
举个例子更好理解:
假如我们把整条道路看成是一个“进程”的话,那由白色虚线分隔开来的各个车道就是进程中的各个“线程”了。这些线程(车道)共享了进程(道路)的公共资源(土地资源)。这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。
使用场景
- 在程序中,如果需要频繁创建和销毁的,使用线程。因为进程创建和销毁开销很大(需要不停的分配资源),但是线程频繁的调用只是改变CPU的执行,开销小
- 如果需要程序更加的稳定安全时,可以选择进程。如果追求速度,就选择线程。
35. 多线程的3种实现方式
1.继承Thread类创建线程类
- 定义Thread类的子类, 并重写该类的run方法, 该run方法的方法体就代表了
线程要完成的任务。 因此把run()方法称为执行体。- 创建Thread子类的实例, 即创建了线程对象。
- 调用线程对象的start()方法来启动该线程
2.通过Runnable接口创建线程类
- 定义runnable接口的实现类, 并重写该接口的run()方法, 该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例, 并依此实例作为Thread的target来创建Thread对象, 该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
3.通过Callable和Future创建线程
- 创建Callable接口的实现类, 并实现call()方法, 该call()方法将作为线程执行体, 并且有返回值
- 创建Callable实现类的实例, 使用FutureTask类来包装Callable对象, 该FutureTask对象封装了该Callable对象的call()方法的返回值。
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值, 调用get()方法会阻塞线程
创建线程的三种方式的对比
- 采用实现Runnable、Callable接口的方式创建多线程时:
- 优势
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。- 劣势
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
- 使用继承Thread类的方式创建多线程时
- 优势
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。- 劣势
线程类已经继承了Thread类,所以不能再继承其他父类。
runnable 和 callable 的区别
- Callcble是可以有返回值的,具体的返回值就是在Callable的接口方法call返回的,并且这个返回值具体是通过实现Future接口的对象的get方法获取的,这个方法是会造成线程阻塞的;而Runnable是没有返回值的,因为Runnable接口中的run方法是没有返回值的;
- Callable里面的call方法是可以抛出异常的,我们可以捕获异常进行处理;但是Runnable里面的run方法是不可以抛出异常的,异常要在run方法内部必须得到处理,不能向外界抛出;
- callable和runnable都可以应用于executors。而thread类只支持runnable
36.并发编程的三大概念:原子性,有序性,可见性
Java 内存模型 (JMM)关键技术点都是围绕着多线程的原子性、可见性、有序来讨论的。JMM 解决了可见性和有序性的问题,而锁解决了原子性的问题。Java内存模型规定了所有的变量都存储在主内存中,每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
原子性:即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized
和Lock
能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java提供了volatile
关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性:即程序执行的顺序按照代码的先后顺序执行。一般来说,处理器为了提高程序运行效率,可能会进行指令重排序,也就是对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。可以通过volatile
关键字来保证一定的“有序性”,volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。另外可以通过synchronized
和Lock
来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
synchronized
关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile
关键字无法保证操作的原子性。
37.并行和并发有什么区别?
举个例子:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停下来接了电话,然后继续吃饭,说明你支持并发。(不一定是同时)
你吃饭吃到一半,电话来了,你一边打电话一边继续吃饭,说明你支持并行。
并发的关键在于有处理多个事件的能力,不一定要同时。并行的关键在于有同时
处理多个事件的能力。
两个区别在于是否是同时
。并发是轮流处理多个事件,并行是同时处理多个事件。
38.守护线程是什么?
定义: 指在程序运行时 在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。通俗点讲,任何一个守护线程都是整个JVM中所有非守护线程的"保姆"。
特点: 守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。当 JVM 中不存在任何一个正在运行的非守护线程时,JVM 进程即会退出,也就是说只要有任何非守护线程还在运行,程序就不会终止,当JVM中只有守护线程运行时JVM会自动关闭。
JVM 中的垃圾回收线程就是典型的守护线程。
使用方法: 在Java语言中,守护线程一般具有较低的优先级,它并非只由JVM内部提供,用户在编写程序时也可以自己设置守护线程,例如:将一个线程设置为守护线程的方法就是在调用start()启动线程之前调用对象的setDaemon(true)方法,若将以上参数设置为false,则表示的是用户进程模式,需要注意的是,当在一个守护线程中产生了其他的线程,那么这些新产生的线程默认还是守护线程,用户线程也是如此。
应用场景:通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,首选守护线程。
39.在 java 程序中怎么保证多线程的运行安全?
总体来说线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,
(atomic,synchronized)。
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile)。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
导致原因:
- 缓存导致的可见性问题
- 线程切换带来的原子性问题
- 编译优化带来的有序性问题
解决办法:
确保线程安全作用是让程序按照我们预期的行为去执行
- JDK Atomic开头的原子类(AtomicInteger,AtomicLong,AtomicBoolean等等)、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题,synchronized和Lock来保证有序性
- 使用java提供的安全类:java.util.concurrent包下的类自身就是线程安全的,在保证安全的同时还能保证性能;
Happens-Before 规则如下:
- 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
40.说一下 synchronized 底层实现原理?
基于对象的监视器(ObjectMonitor),在同步方法执行前后,有两个指令,进入同步方法前monitorenter,方法执行完成后monitorexit;
任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为 BLOCKED 。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。
补充:一个synchronize锁会有两个monitorexit,这是保证synchronize能一定释放锁的机制,一个是方法正常执行完释放,一个是执行过程发生异常时虚拟机释放;
synchronized可以用在如下地方
- 修饰实例方法,对当前实例对象this加锁
- 修饰静态方法,对当前类的Class对象加锁
- 修饰代码块,指定加锁对象,对给定对象加锁
41.synchronized 和 volatile 的区别是什么?
1、
volatile
本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2、volatile
仅能使用在变量级别;synchronized
则可以使用在变量、方法、和类级别的
3、volatile
仅能实现变量的修改可见性,不能保证原子性;而synchronized
则可以保证变量的修改可见性和原子性
4、volatile
不会造成线程的阻塞;synchronized
可能会造成线程的阻塞。
5、volatile
标记的变量不会被编译器优化;synchronized
标记的变量可以被编译器优化
42.synchronized 和 Lock 有什么区别?
1、synchronized不需要手动释放锁,lock需要在锁用完后进行unlock释放锁;
2、synchronized只能是默认的非公平锁,lock可以指定使用公平锁或者非公平锁;
3、lock提供的Condition(条件)可以指定唤醒哪些线程,而synchronized只能随机唤醒一个或者全部唤醒;
43.多线程锁的升级原理是什么?
JVM优化synchronized的运行机制,当JVM检测到不同的竞争状态时,就会根据需要自动切换到合适的锁,这种切换就是锁的升级。升级是不可逆的,也就是说只能从低到高,不能够降级。
锁的级别从低到高:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
锁分级别原因:
没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改以上是关于整理了Android面中常见的62个Java知识点...的主要内容,如果未能解决你的问题,请参考以下文章