java面试学习 日更
- java面试学习(内容来自网络拼凑 部分内容自己改写)
- Day_01
- Day_02
- Day_03
- Day_04
- Day_05
- try{}方法中写有return 那么紧跟在try{}方法后的final{}是否还会被执行?
- 进程和线程的区别是什么?
- 什么是死锁?
- Thread类的sleep()方法和对象的wait()方法都可以让线程短暂停止运行 它们有什么区别?
- 什么是线程池 Thread pool?
- 编写多线程程序有几种方法?
- 同步方法和同步代码块有什么区别
- 迭代器iterator是什么?
- Iterator 和 ListIterator有什么区别
- 怎么确保一个集合不能被修改
- 并行和并发的区别
- 守护线程是什么
- 创建线程的三种方式
- 说一下Runnable和Callable的区别
- 线程有哪些状态
- notify和notifyall()有什么区别
- start()和 run()方法的区别
- 创建线程池有哪几种方式?
- 线程池都有哪几种状态?
- 线程池中submit() 和 execute()的区别
- java中怎么保证多线程的运行安全
- 多线程中 synchronized锁升级原理
- MVC
- SpringMVC
- 引用学习网站
java面试学习(内容来自网络拼凑 部分内容自己改写)
Day_01
- jdk 和 jre的区别
JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。
JRE:Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。
- == 和 equals的区别
== 的比较类型有两种:值比较和引用比较。
基本类型的值是固定的 引用地址也应该是一样的;
引用比较会因为不同的地址空间而做出判断
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x == y); // true
System.out.println(x == z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
实际上equals的作用一直都是==的比较,但由于现在String integer等方法都重新改写了equals,本来应该是引用比较却给我们的印象一直是值比较。
-
equals和hashcode的联系
说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值
hashCode值相等只能说指向对象是一致的但是不能代表值也相等 值相等需要equals方法再去验证
即:hashcode相等 equals不一定相等
equals相等 hashcode一定要相等
一般来说 hashcode应当要与equals效果保持一致。 -
三集合 hashSet hashMap hashTable
- hashMap
是个键值对数据类型
一个是键 一个是键值(引用)
键组成数组 数组的元素引用键值 键值由链表的形式表现 键值的替换并非是删除覆盖 而是通过链表的形式 将新的键值放在链头 旧值向后延申
- hashMap
数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。
哈希表((Hash table)既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; // 键无法更改
V value; //键值可以
Entry<K,V> next; // 每一个entry都有指向下一个entry的指针next
final int hash; //每次计算的hash是一定的
……
}
–hashMap的元素储存put方法
public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,证明根据hash值找到的索引数组中已经存在一个键值对,通过循环不断遍历 e 元素的下一个元素,展开链表。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 确认数组中i处的旧元素 哈希值和key值保持一致 确认找到准确位置
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
// 将本来存在的第一个键值储存下来
V oldValue = e.value;
//将要储存的键值替换到链表的第一个位置
e.value = value;
e.recordAccess(this);
//返回这个旧值 以此类推可以将本来存在的链表都向后延申一位
//达到新加入的元素处于链头 旧元素依次向后一位
return oldValue;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}
储存实现过程:拿到新的键值对以后先通过键的hashcode去计算新的哈希值,带着这个哈希值再去找数组中应该存放的位置也就是索引i ,找到以后先判断索引i处是否存在元素 如果不存在直接储存 存在的话就将链表延申 新元素加入表头。
addEntry是hashMap提供的一个包访问工具 将数据放入table并指向新的entry
void addEntry(int hash, K key, V value, int bucketIndex) {
// 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry (这里的指向可以理解将entry的元素放入数组)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 对的数量超过了极限
if (size++ >= threshold)
// 把 table 对象的长度扩充到原来的2倍 table长度应当是2的整数倍。
resize(2 * table.length);
}
另外,hashTable一般都为2的整数倍,目的是为了每个元素可以均匀的储存在数组中而不是挤在同一位置的链表中,少了查询链表的步骤可以减少查询时间,同时也可以保证数组的每一个位置都可以有储存的机会。
hashMap储存(put方法)总结:
根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部。
–hashMap的读取get()方法
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
学习了put方法后,get方法看起来就很容易理解了。hashCode获取entry位置 遍历位置处的链表 key与查询key一致,返回key的键值 。
hashMap和hashTable大体上是一致的,有一些细节是有区别的这里先放下。
–hashSet
Day_02
final 在java中的作用(final 类 方法 变量固定不可变)
1、final修饰的类是最终类,不能被继承。
2、final修饰的变量是常量且必须初始化,初始化后不能修改。
3、final修饰的方法不能重写。
math.round(-1.5) == -1
无论正数或者负数或者负数,运算规则相同,都是在给出数值的基础上+0.5,然后向下取整。
String属于数据类型吗?
不属于,数据类型有八种:int shot long char float boolean double byte。
除了基本类型(primitive type)和枚举类型(enumeration type),剩下的都是引用类型(reference type),String 属于对象。
操作字符串有哪些类?它们有那些区别?
三种类:String StringBuilder StringBuffer。
String对象每申请一次需要创建新的内存空间,指针重新指向新的内存空间。
而 StringBuilder 和 StringBuffer 每次对象申请是将之前对象的内容修改 但指针仍然指向之前的内存空间,后两者多应用于字符串需要多次改变。
StringBuider 和 StringBuffer 前者为非线程安全 后者为线程安全 。线程安全需要多线程同步安全进行,因此StringBuilder在性能上(单线程)可以选择。
String str = “i” 和 String str = new String(“i”)不同吗
前者为常量申请 后者为对象申请。
前者会被jvm虚拟机分配到常量池,后者被分配到堆内存中。
如何将字符串反转?
用 StringBuilder 或 StringBuffer 的reverse()。
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer. append("abcdefg");
System. out. println(stringBuffer. reverse()); // gfedcba
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder. append("abcdefg");
System. out. println(stringBuilder. reverse()); // gfedcba
String类的常用方法有哪些?
indexOf():返回字符在字符串中的索引
charAt():返回索引处的字符
split():分割字符串
toUpperCase():大写
toLowperlCase():小写
trim():消除字符串两边空白
replace():替换字符串
getBytes():得到字节类型的字符串
length():得到字符串长度
substring():得到字串
equals():字符串比较
抽象类 抽象方法 abstract
很多时候我们可以提前构造功能函数,写好方法,其他类可以直接继承调用,但有时方法的具体实现是不确定的,所以可以只申明有此方法,但不给与具体的执行,这就是抽象。
抽象类一定要有抽象方法吗?
不一定 ,类可以是抽象的,方法有没有抽象不影响。但有抽象方法的类必须申明抽象类。
普通类和抽象类的区别
普通类有具体的执行过程,且不含有抽象方法,可以被直接实例化。
抽象类不是,需要被继承重写后再去实例化。
抽象类不能用final修饰u
final一旦修饰 抽象类不可再更改 就是去了继承重写的意义,编辑器也不允许。
接口和抽象类(继承)的区别
1、实现上 抽象类子类继承要用extends 接口使用implements。
2、构造函数上 抽象类可以有构造函数 但接口不能有
3、一个类可以实现多个接口但只能继承一个抽象类
4、接口中的方法默认使用public访问修饰符,抽象类不限制访问修饰符。
java 中 IO流分为几种?
按功能 输入流(input) 输出流(output)
按类型来分 字节流 和 字符流
字节流和字符流的区别: 字节8位传输 字符16位传输。
Day_03
Files常用函数
Files.exists() 查看文件是否存在
Files.copy() 复制文件
Files.delete() 删除文件 或目录
Files.createFile() 创建文件
Files.createDirectory() 创建文件夹
Files.move() 移动文件
Files.size() 查看文件个数
Files.read() 读入文件
Files.write()写入文件
java有那些容器
java 容器分为两大类 :collection和Map
Colleciton:{
List:{
ArrayList:”变长动态数组“,
LinkedList:“双向链表”,
Vector:“队列”,
Stack:“栈”
},
Set:{
HashSet:”不保证元素顺序的去重“,
LinkedSet:”保证按照原来的顺序去重“,
TreeSet:”自然排序(升序去重)、定制排序(自定义)“
}
}
Map:{
HashMap:LinkedHashMap,
TreeMap,
ConcurrentHashMap,
HashTable
}
collection 和 collections的区别
collection:Collection是一个接口,它是Set、List等容器的父接口;
collections:是个一个工具类,提供了一系列的静态方法来辅助容器操作,like:collections.sort(list)。
List Set Map的区别
区别主要体现在两个方面:元素是否有序 元素是否可重复
HashMap 和 HashTable区别
存储:HashMap允许Key -value 为null ;HashTable 不允许
线程安全:HashTable 线程安全 ;HashMap非线程安全
推荐:单线程用HashMap 多线程用concurrentHashMap。
如何决定使用HashMap还是TreeMap
HashMap方便查找 插入 删除 (key的hashcode值容易得到) 若需求对Key集合有序遍历 使用TreeMap
说一下 HashMap的实现原理
HashMap 基于Hash算法实现,put(key,value)写入,get(key)读出,根据key.hashcode()计算Hash值,再根据hash值将value存入表中,需要说明的是不同的key值也可能得到相同的hash值,也就是hash冲突,通常冲突少时,采用顺序表加链表的形式将多value存入链表,否则采用红黑树储存。
说一下HashSet的实现实现原理
HashSet底层是基于HashMap实现的,实际上也可以说仍然由HashMap储存,将Set值储存在Map的key中,而value使用一个static final的Object对象标识。
ArrayList 和 LinkedList的区别
存储:ArrayLIst是动态数组 类似与顺序表(查询时间复杂度O(1)),而LinkedList是双向链表结构(查询时间复杂度O(n))。
随机访问效率:随机访问肯定是数组(顺序表)好很多 。
插入删除:插入删除肯定是链表好很多。
总结:多查询 ArrayList 多插入删除LinkedList
数组Array和列表list之间的转换
数组转list :Array.asList(array)
list转数组:List.toArray(list)
ArrayList 和 vector区别
ArrayList 非线程安全 vector 线程安全
性能:ArrayList优秀与Vector
扩容:两者都是可扩充容量,Vector每次扩充一倍,ArrayList只扩充50%。
Array和ArrayLIst的区别
Array定长 ArrayLIst属于list 可变长
Array可储存基本数据类型和对象,List只能储存对象
ArrayList内置方法更多
在队列(Queen)中 poll()和remove()区别
相同:都是返回队列第一个元素,并且删除对头。
不同:当队列空元素时,poll返回null 但remove抛出异常。
那些集合类是线程安全的
安全:Vector hashtable stack Properties
而非安全的hashMap也会有对应的线程安全类,eg:concurrentHashMap。
Day_04
是否可以继承String类
不可以 String是final类。
String str = new String(‘ss’)会创建几个对象?
两个。这种方式创建字符串时,首先jvm会在常量池中寻找有没有已经创建好ss这个字符串常量,如果有就不再创建,否则创建新的字符串常量,碰到new时,会在内存(堆)中去创建对应该字符串的对象 Str,所以是两个对象。
jvm是如何加载.class文件的?
首先我们知道java的特点是跨平台,只需要一次将程序javac编译成.class文件,即可在后续不同平台使用中解释成对应的执行语句,jvm加载class文件是利用类加载器(classLoader)将class文件从硬盘转入内存,常量初始化,符号引用转为直接引用。另外,实际代码运行过程前需要预编译,将jre中的常用基础库提前加载方便使用,减少io操作,而我们自己写的类加载称为按需加载。
hashMap时间复杂度
1、表里没有要插入的key,直接插入O(1)
2、表里有要插入的key,且链表个数不多于6个,插入链表需要O(1)+O(n)
3、表里有要插入的key,且链表个数多于6个,采用红黑树存入value 需要O(logn)
java中exception和error有什么区别?
exception是用户可以捕获到的异常,而error是不期待被用户捕获的异常。
throw 和throws的区别
throw表明 该情况一定会抛出异常。
throws表明 该方法可能会抛出异常,一个或多个,提醒使用者注意。
列出一些常见的异常
- ArithmeticException(算术异常)
- ClassCastException (类转换异常)
- IllegalArgumentException (非法参数异常)
- IndexOutOfBoundsException(下标越界异常)
- NullPointerException (空指针异常)
- SecurityException (安全异常)
Day_05
try{}方法中写有return 那么紧跟在try{}方法后的final{}是否还会被执行?
会执行,按照代码顺序 会先执行try中的return 此时函数结果保存在函数栈中 并不会立即返回调用者,继续执行final函数 如果final函数中也有return 则直接返回final中的结果 否则返回之前try的结果。
进程和线程的区别是什么?
进程是执行中的程序 而线程只是进程中的一个执行序列 一个进程中会有多个线程各司其职。
什么是死锁?
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
Thread类的sleep()方法和对象的wait()方法都可以让线程短暂停止运行 它们有什么区别?
sleep()方法是线程类的静态方法 它只是将线程某时间段的时间片拿走 等时间片结束后又会恢复到之前的就绪状态,等待下一次执行。
wait()方法是对象的方法,会剥夺对象的锁并放入对象的等待池,直到重新唤醒线程(notify方法)进入到等锁池,若能重新得到锁就可以回到就绪状态。
- 类的不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
什么是线程池 Thread pool?
面向对象编程中,创建和销毁对象会消耗很多资源,在java中 jvm会试图跟踪每一个对象 销毁的时候作垃圾回收。
而线程池顾名思义是提前创建一个 拥有几个可执行线程的池子 在需要创建线程对象的时候拿出来用 不需要的时候也不用销毁而是继续放回池中。
编写多线程程序有几种方法?
两种。 继承Thread类和实现Runable接口。两者都需要重写run方法来定义线程的行为。
推荐后者 因为一个类可以实现多个接口但只能继承一个类。
同步方法和同步代码块有什么区别
在java中 每一个对象都有一个锁。
同步方法是给方法加synchronized同步锁,而同步代码块是在方法内部加锁的。
就性能而言 同步的范围越小 性能越好。
迭代器iterator是什么?
迭代器iterator接口提供遍历每一个collectiion的方法,某一个collection通过使用迭代器方法得到迭代器实例,另外迭代过程中允许移除元素。
List<String> list = new ArrayList<>();
Iterator it = list.iterator();
while(it.hasNext()){
String obj = it.next();
System.out.println(obj);
}
Iterator 和 ListIterator有什么区别
Iterator可以遍历set和list(collection),而ListIterator只能遍历list。
Iterator只能单向遍历列表 而ListIterator可以双向遍历。
ListIterator继承于Iterator接口,可以说是为list提供的接口,所以还提供了额外的一些列表操作方法。
怎么确保一个集合不能被修改
利用Collecitons.unmodifiableCollection(Collection c)方法创建只读集合。
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
并行和并发的区别
并行:多核处理器同时处理多个进程。
并发:单核处理器按照时间片分片的方式同时处理多个进程,逻辑上的并发依赖于处理速度和时间片大小。
守护线程是什么
守护线程是运行在后台的特殊现场,它独立于控制终端周期性的执行某种任务或者等待某件事的法生。java的垃圾回收就是特殊的守护线程。像是人体的白细胞。
创建线程的三种方式
1、继承Thread类 重写run()方法
2、实现Runnable接口
3、实现Callable接口
说一下Runnable和Callable的区别
Runnable 没有返回值 Callable有返回值。Callable可以说是作为了Runnable的补充了。
线程有哪些状态
NEW :线程未启动
RUNNABLE :正在执行中
BLOCKED:阻塞中
WAITING:等待中的
TIMED_WAITING:等待一定时间再重新被唤醒 ---- sleep()
TERMINATED:执行结束的
notify和notifyall()有什么区别
notify()只唤醒一个线程,有jvm指定。
notifyAll()会唤醒等待池中所有的线程,所有线程竞争锁,得到锁的线程可继续执行,其他线程等待下一次锁的释放。
所以锁也是一种临界资源。
start()和 run()方法的区别
start()用于启动线程,只能执行一次。
run()方法用于执行线程内部代码,可以多次执行,所以不同的线程执行代码可以实现多线程。
创建线程池有哪几种方式?
- newSingleThreadExecutor():特点在于池中的工作线程数目被限制为1,也就是说所有的线程只能按顺序执行,并且不允许更改。
- newCatchThreadPool():特点在于线程缓存机制。它会试图保存缓存线程重用,如果当前没有缓存线程就会创建新的线程执行,线程执行完毕后不会立即销毁,而是作为缓存线程等待60s,时间段内无工作才会移除缓存销毁,另外长时间的缓存也不会耗费很多资源,所以是一种处理大量短时间任务的线程池。
- newFixedThreadPool():与newSingleThreadExecutor()不同的是,它支持最多n个线程处于活动状态,不够就创建,够了就等待。
- newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度。
- newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
- newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
- ThreadPoolExecutor():是最原始的也是最核心的线程池创建方式,上面的1-3是对其的封装。
线程池都有哪几种状态?
RUNNING:这是最正常的状态,接收新的任务,处理等待队列的任务。
SHUTDOWN:不接受新的任务,但是会继续处理等地队列的任务。
STOP:不接受新的任务,不处理等待队列的任务,终止正在执行的任务。
TIDYING:所有任务都销毁了,并且工作线程数为0,状态转换为TIDYING并执行钩子函数termidated().
TERMINATED:钩子函数执行结束后就是该状态表示线程池结束工作。
线程池中submit() 和 execute()的区别
submit()可以执行Runnable和Callable类型的任务。
execute() 可以执行Runnable类型的任务。
java中怎么保证多线程的运行安全
- 使用安全类
- 使用synchronized自动锁
- 使用手动锁lock
多线程中 synchronized锁升级原理
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
是一个资源优先级变更的过程。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
MVC
SpringMVC
流程:
页面请求转到前端控制器DsipatcherServlet,前端控制器内部有路由表HandlerMap 可以通过请求的url找到对应的modelHandler(也就是我们编写的处理业务的代码页/块),拿到HandlerMap以后 通过页面控制器HandlerAdapter控制页面跳转 再跳转至具体的modelHandler之前 还需要对请求中传过来的数据进行检查,之后传入Handler,handler层结束业务执行后由页面控制器将模型及视图都重新传入前端控制器,前端控制器会将模型传入视图,并通过viewResovler渲染视图。最后返回给页面。
引用学习网站
https://www.cnblogs.com/51ma/p/12462688.html
https://www.bilibili.com/read/cv558137/
https://zhuanlan.zhihu.com/p/44185603
https://blog.csdn.net/sinat_35512245/article/details/59056120/
https://blog.csdn.net/a1365596149/article/details/106711097