1. 实例方法和静态方法有什么不一样?
1)实例方法
- 实例方法属于对象,通过 实例对象.方法名(参数)调用。
- 允许使用静态成员
2)静态方法:
- 属于类,通过类名.方法名
- 只能使用类的静态成员,而不能直接使用非静态成员
3)相同点
- java的所有方法,都被编译成字节码,作为类的类型信息保存在.class文件
- jvm保存的所有方法信息都在方法区中。因此,方法区在加载方法信息时是统一对待,无论静态方法或实例方法,都在类第一次被使用时加载,时机上没有任何区别。
2. java中的异常分为哪几类?怎么使用?
首先,需要了解error和exception的区别。error是一个应用程序不能捕获的严重错误,一般来说开发人员无法处理,并且归类为unchecked-exception,例如内存溢出(stackoverflow),而异常主要是java编译和运行时的异常。
其次,了解一下error和exception的相同点,他们有共同父类Throwable。
最后,了解一下异常分类,及各部分的使用
checked-exceptions: 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
unchecked-exceptions:error,编译时异常(例如:NOP)
顺便说一下,try catch finally执行顺序:
try->发生异常时catch->finally,return按照位置执行
当return在try中时,调用return,会将计算后的值存储到调用方栈帧的操作数栈栈顶,执行finally的时候,方法还没有退出,退出后,将当前栈帧出栈。因此,这个值为引用类型时,finally中修改值会改变return的值;这个值为基本类型时,finally修改不会影响return的值。
当return在catch中时,和在try中相同。
当return在finally中时,将会覆盖函数中其他的return语句(会直接在finally中退出,导致try、catch中的return失效)
public static int demo4() {
int i = 0;
try {
i=1;
return i;//输出 finally trumps return. 1
}catch (Exception e){
i=2;
return i;
}
finally {
i = i+1;
System.out.println("finally trumps return.");
//return i;输出finally trumps return. 2
}
//return i;//输出 finally trumps return. 2
}
//输出结果
finally trumps return.
1
3. 常用的集合类有哪些?比如List如何排序?内部实现和使用场景?
4. 内存溢出是什么?
勘误:图中head应该是heap
5. ClassLoader的作用?
class Loader是一个抽象类,主要用于加载class对象,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。该类中loadClass()方法描述了如何加载一个class,原理为双亲委派模型,并且是线程安全的(使用sychronized关键字)。
双亲委派模型原理:当某个类加载器收到类加载请求时,首先将类的加载任务委派给父加载器,递归调用,如果父类加载器没有完成任务,调用启动类加载器,如果还未成功,使用自己的类加载器。
6. ==和equals的区别?
a. ==
基本数据类型(int,char,short...)比较的是值,引用类型比较的是引用
b. equals
所有类从Object类中继承equals方法,object.equals的内部实现为public boolean equals(Object obj) { return (this == obj); },因此在没有重写equals方法时和==并无差别。
equals方法本身比较的是引用,但是在一些类中重写了该方法,重写的逻辑可能是将地址比较改成了值比较,比如String,Interger类。因此,对一个对象进行值相等进行比较时,需要重写equals方法
7. hashCode方法的作用?
hashcode主要用于查找,根据对象的某个属性,使用散列算法存储(比如取余法)到数组的某个位置上,这样取数据时,可以直接根据对应的算法取数据
(1)两个对象equals为true,那么hashcode一定相等(jdk规定,因此重写equals方法时一般也要重写hashcode方法)
(2)两个对象的hashcode相等,但equals不一定为true(因为equals的实现有很多种)
8. Object类中有哪些方法?列举3个以上。
equals,hashcode,clone,split,valueOf,notify,wait
9. NIO是什么?适用于何种场景?
(这篇文章讲的很容易看懂)
(1)首先先了解IO的作用是什么,当程序想要从文件中读取数据时,需要在他们之间建立一条数据通道,IO中该通道是流。
(2)NIO中的新的名词:管道(Channel),缓冲(Buffer) ,选择器( Selector)
Channel:任何类型的数据传输,必须经过有一个渠道。BIO中的流就相当于数据传输的一个渠道。管道实际上就像传统IO中的流,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。BIO中数据传输是单项的,例如输入流inputstream和outputstream,而channel是双向的,输入输出都用channel
Buffer:实质上是一个容器对象。(读取和写入的数据都要放到buffer容器中再被使用)
Selector:选择器用于监听多个管道的事件,使用传统的阻塞IO时我们可以清楚什么时候可以进行读写,而使用非阻塞通道,我们需要一些方法来知道什么时候通道准备好了,选择器正是为这个需要而诞生的。Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。(可理解为多路复用器,每个客户端连接就是一个SocketChannel,这些SocketChannel会在Selector上注册,并且设置对各个Channel感兴趣的事件。当Selector监听到对应的事件之后,其就会将事件交由下层的线程处理)
面向块(buffer中)的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
(3)NIO适用场景
服务器需要支持超大量的长时间连接。比如10000个连接以上,并且每个客户端并不会频繁地发送太多数据。例如总公司的一个中心服务器需要收集全国便利店各个收银机的交易信息,只需要少量线程按需处理维护的大量长期连接。
Jetty、Mina、Netty、ZooKeeper等都是基于NIO方式实现。
旧的IO处理方式
InputStream input = ... ; // get the InputStream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
NIO处理方式
//第一种示例
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(! bufferFull(bytesRead) ) {
bytesRead = inChannel.read(buffer);
}
//第二种完整示例
File file = new File(bigExcelFile);
//Get file channel in readonly mode
FileChannel fileChannel = new RandomAccessFile(file, "r").getChannel();
//Get direct byte buffer access using channel.map() operation
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
// the buffer now reads the file as if it were loaded in memory.
System.out.println(buffer.isLoaded()); //prints false
System.out.println(buffer.capacity()); //Get the size based on content size of file
//You can read the file from this buffer the way you like.
for (int i = 0; i < buffer.limit(); i++)
{
System.out.print((char) buffer.get()); //Print the content of file
}
BIO适用场景
适用于连接数目比较小,并且一次发送大量数据的场景,这种方式对服务器资源要求比较高,并发局限于应用中。
10. HashMap数据结构、扩展策略,Hash冲突攻击如何防范,如何实现线程安全的HashMap?
(1)数据结构:数组+链表的形式
(2)默认长度值设为16
因为在计算index时,需要使用key.hashcode&(length-1)
当length为16时,length-1为15是二进制的1111,这个时候index的位置就是hashcode的后四位数的值。
假如把默认值设为10,那么hashcode和length-1相与,得到的index中均为奇数,这样偶数位置就浪费了,而且hash冲突的概率会大大增加,也就是说如果hashcode分布均匀,那么在table上分布就均匀了
(3)扩容策略
HashMap.Size >= Capacity * LoadFactor时就进行扩容
注:低位指的是新数组的 0 到 oldCap-1 、高位指定的是oldCap 到 newCap - 1
扩容为原来的两倍,jdk8中resize不再每一个key都重新计算hash值,规则改为:使用当前元素的hash&扩容前tablecapcity,如果该值等于0,则认为hash值小于扩容前tablecapcity,那么hash值不会改变,所以不需要重新计算hash值,该元素放在原来的位置上;
如果该值不等于0,则该元素应该在新数组的高位位置上,且高位的元素组成的链表放置的位置只是在原有位置上偏移了老数组的长度个位置;代码为: newTab[j + oldCap] = hiHead;
扩容规则为,先扩容再添加数据,新添加的数据都会重新计算hash值和index,扩容只涉及到老数组的数据迁移到新数组中,这些老的数据没有必要每一个都计算一遍hash值和index,利用再原数组的index就可以得到在新数组中的位置
计算index的算法为 hash&(n-1)
扩容前数组长度为16
扩容后数组长度为32
那么当hash&(扩容前数组长度n)=0时,说明hash&(新数组长度-1)时,得到的值(index)是不会变化的,所以不需要重新计算index;
当hash&(扩容前数组长度n)!=0时,说明,hash值的高位会影响 hash&(新数组长度-1)在新数组中的index。
并且,高位的元素组成的链表放置的位置只是在原有位置上偏移了老数组的长度个位置。
例:hash为 17 在老数组放置在0下标,在新数组放置在16下标; hash为 18 在老数组放置在1下标,在新数组放置在17下标;
(4)Hash冲突攻击如何防范
hash冲突攻击是指,黑客恶意伪造相同hash值的数据,造成该位置的hash链路过长,查找时效率低下。java8中采用了红黑树来预防该问题,红黑树的查找效率为log(n),比链式查找n优化了很多。其他防止黑客攻击的方法还有很多,我了解的不是很多,一个简单的方式有:加salt;哈希值的计算变为 hash(salt + str),salt为特殊的字符串,一般都是固定值,由于salt是不对外暴露的,所以黑客构造哈希值完全相同的字符串的难度就增大了
(5)线程不安全
在put数据时,多个线程同时在同一个位置插入元素时会出现后面线程覆盖前面线程结果的情况。
在java7中,resize方法时,会重新计算每个元素的位置,在链表中插入时,由于hash链表采用的是头插法,此时会出现环形链造成死循环,java8中已改为尾插法,解决了死循环问题;
怎么使hashmap线程安全呢?方法有三种:hashtable,Collections.synchronizedMap,concurrentHashmap,但效率最高的时concurrentHashmap,它使用了分段锁技术(该类包含的知识点还挺多的,可能会作为面试的下一个考察点)
(6)hashmap中key,value都可以为null,HashTable的key value都不能为null
key=null的代码
/**
* Offloaded version of put for null keys
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
hashmap计算hash值的时候return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);会进行判断,所以key最多只能一个为null,后面put的key=null会覆盖前面的值。hashtable计算hash值的时候int hash = key.hashCode();直接取的hashcode,key为null报错,value=null直接报空指针异常。
(7)hash值怎么计算的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。
大家都知道上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。源码中模运算代码也很简单,就是把散列值和数组长度做一个与运算
(n - 1) & hash
但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。“扰动函数”的价值就体现出来了,说到这里大家应该猜出来了。看下面这个图,
右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
具体实现上,由两个方法int hash(Object k)和int indexFor(int h, int length)来实现。
hash :该方法主要是将Object转换成一个整型。
indexFor :该方法主要是将hash生成的整型转换成链表数组中的下标。
为了聚焦本文的重点,我们只来看一下indexFor方法。我们先来看下Java 7(Java8中虽然没有这样一个单独的方法,但是查询下标的算法也是和Java 7一样的)中该实现细节:
static int indexFor(int h, int length) {
return h & (length-1);
}
indexFor方法其实主要是将hashcode换成链表数组中的下标。其中的两个参数h表示元素的hashcode值,length表示HashMap的容量。那么return h & (length-1) 是什么意思呢?
其实,他就是取模。Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。
位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。
那么,为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
X % 2^n = X & (2^n – 1)
假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。
此时X & (2^3 – 1) 就相当于取X的2进制的最后三位数。
从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。
简而言之,就是两个运算都是取的后n位数的值。
11. NIO模型,select/epoll的区别,多路复用的原理
select/epoll的区别:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
多路复用原理:I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作
参考资料:
https://www.jianshu.com/p/db5da880154a
12. Java中一个字符占多少个字节,扩展再问int, long, double占多少字节
一个字符(char)占2个字节,short(2)int(4)long(8)float(4)double(8)boolean(0和1)byte(1)
float和double的区别:使用时需要确认精度的区别,float有效位数为8,double有效位数为16,除此之外当使用float f= 5.4;时会报错,因为5.4默认时double类型的。需要使用float f= 5.4f;
public static void main(String[] args) {
System.out.println(3*0.1);
System.out.println(3*0.1==0.3);
//float是8位有效数字,第7位数字将会四舍五入
float a =1.32344435f;
System.out.println(a);
}
13. 创建一个类的实例都有哪些办法?
(1) new的方式
new Student();
(2) 反射的方式
Class clazz = Student.class;
Student s = (Student) clazz .newInstance();
这种方式只能使用无参的构造方法创建实例,当一个类中只有有参构造器时会报错,若需要使用时,需要在类中添加无参构造器;
可采用一下方式使用有参构造器;
Class clazz=Dog.class;
Constructor constructor=clazz.getConstructor(String.class,int.class});
Dog dog=(Dog) constructor.newInstance("xiaohei",3});
(3) clone
类实现cloneable接口,重写clone方法;
并且,clone出来的对象和原对象拥有不同的地址;
(4) 通过对象反序列化的方式
序列化对象必须实现Serializable这个接口
把对象转为字节序列的过程称为对象的序列化
把字节序列恢复为对象的过程称为对象的反序列化
1)将对象的字节序列永久的保存到硬盘上
例如web服务器把某些对象保存到硬盘让他们离开内存空间,节约内存,当需要的时候再从硬盘上取回到内存中使用
2)在网络上传递字节序列
当两个进程进行远程通讯的时候,发送方将java对象转换成字节序列发送(序列化),接受方再把这些字节序列转换成java对象(反序列化)
当Dog类实现了Serializable接口后,我们现在将Dog对象序列化
Dog dog=new Dog();
dog.name="xiaohei";
dog.age=3;
FileOutputStream fos = new FileOutputStream("dog.txt");
ObjectOutputStream ops = new ObjectOutputStream(fos);
ops.writeObject(dog);
System.out.println("dog对象序列化完成");
通过ObjectOutputStream的writeObject方法,我们就将一个对象完成了序列化
现在我们再次将刚才序列化后的对象反序列化回来,这次用到的是ObjectInputStream的readObject方法:
FileOutputStream fos=new FileOutputStream("dog.txt");
ObjectInputStream ois=new ObjectInputStream(fos);
Dog dog=(Dog) ois.readObject();
System.out.println("我叫"+dog.name+"今年"+dog.age+"岁了");
System.out.println("对象反序列化完成");
这样我们就使用了对象的序列化完成了java对象的创建
14. final/finally/finalize的区别?
(1)final
修饰变量:除了初始化阶段,该变量不可修改;修饰对象时,该对象引用的地址不能修改,值可以修改,例如可以修改student.name="change",不能这样修改修改student=studentb;不能修改String s;s="a";
修饰方法:该方法不能被子类继承;高效,编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。
修饰类:该类不能被修改,不能被继承
(2)finalize()
finalize()方法在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收,类似于C++中的析构函数;用户自定义时,用于释放对象占用的资源(比如进行I/0操作);
(3)finally
与try{}配合使用,不论try中的代码执行完或没有执行完(这里指有异常),该代码块之中的程序必定会进行
15. LinkingBlockingQueue与ArrayBlockingQueue的区别,他们的适用场景?
在创建线程池时,会使用一些封装好的方法,比如newFixedThreadPool(int nThreads),但实际使用时还是需要明白创建一个线程池时各个参数的含义。
/**
* @param corePoolSize
* 核心线程池大小,除非设置了allowCoreThreadTimeOut,否则就要保持线程池的线程数,即使他们是空闲的
* @param maximumPoolSize
* 线程池允许的最大线程数
* @param keepAliveTime
* 线程存活时间。当线程的数量超出核心线程池大小时,这是空闲线程终止当前任务,等待新任务的最长时间
* @param unit
* 线程存活时间单位
* @param workQueue
* 暂存任务的工作队列。这个队列只能暂存继承Runnable接口,且使用execute方法执行的任务
* @param threadFactory
* 线程工厂,用来创建线程的
* @param handler
* 当任务被阻塞时的处理器,可能是由于线程数和任务队列容量超出范围
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
首先,介绍一下线程池对任务的处理过程
然后,介绍一下工作队列的含义
(1)ArrayBlockingQueue
有界阻塞队列,遵循先进先出的规则。
该队列必须设置队列长度。
使用一个锁控制插入和删除。
ArrayBlockingQueue内部,因为是直接使用数组空间的,而且都是预先分配好的,所以操作没有那么复杂。它使用notEmpty控制出队,通过notNull控制入队,其实该队列的模式类似于生产者消费者模式,因此通过notEmpty和notNull条件队列控制队列满和队列为空的情况。
(2)LinkedBlockingQueue
无界链表,遵循先进先出的规则。
该队列默认长度为Integer.MaxValue。
使用了2个锁来控制,一个名为putLock,另一个是takeLock,但是锁的本质都是ReentrantLock。因为LinkedBlockingQueue使用了2个锁的情况下,所以在一定程度上LinkedBlockingQueue能更好支持高并发的场景操作,同时也因此具有更优的吞吐量。
通过链表进行维护,采用动态内存,无论是分配内存还是释放内存(甚至GC)动态内存的性能自然都会比固定内存要差;而且每次插入的对象还要转为Node<>(e)对象。
16. Session/Cookie的区别?
Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
(1)session是如何创建的呢?
在服务器端程序运行的过程中创建的,不同语言实现的应用程序有不同创建Session的方法,而在Java中是session在访问tomcat服务器HttpServletRequest的getSession(true)的时候创建;
tomcat的ManagerBase类提供创建sessionid的方法:随机数+时间+jvmid;
存储在服务器的内存中,tomcat的StandardManager类将session存储在内存中,也可以持久化到file,数据库,memcache,redis等。
客户端只保存sessionid到cookie中,而不会保存session,session销毁只能通过invalidate或超时,关掉浏览器并不会关闭session。
这个Session id在随后的请求中会被用来重新获得已经创建的Session;在Session被创建之后,就可以调用Session相关的方法往Session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有Session id;当客户端再次发送请求的时候,会将这个Session id带上,服务器接受到请求之后就会依据Session id找到相应的Session,从而再次使用之。
(2)session删除:超时;程序调用HttpSession.invalidate();程序关闭;
(3) session存放在哪里:服务器端的内存中。不过session可以通过特殊的方式做持久化管理(memcache,redis)
17. String/StringBuffer/StringBuilder的区别,扩展再问他们的实现?
(1)区别
String字符串对象不能修改,注意在jdk1.5以后string中的+的内部实现改为append,因此在写代码时,简单的字符串拼接直接用string的+即可,但是如果在for循环中用字符串拼接时,还是要使用stringBuilder,因为string的加号会创建StringBuilder对象,在for中使用时会创建大量的对象,此时还是一个stringBuilder比较好。String中的对象是不可变的,也就可以理解为常量,显然线程安全
stringBuffer字符串对象可以修改,线程安全的
StringBuilder字符串对象可修改,非线程安全
(2)实现
String
1)String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。在Java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。
2)String类其实是通过char数组来保存字符串的。
3)String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象
下面有两个例子:
String a = "a1";
String b = "a"+1;
此时a==b为true,因为a,b在编译期间就可以确定值
String a = "a";
String c = "1";
String b = a+c;
此时a==b为false,因为b在编译期间不能确定值
总结来说就是:字面量"+"拼接是在编译期间进行的,拼接后的字符串存放在字符串池中;而字符串引用的"+"拼接运算实在运行时进行的,新创建的字符串存放在堆中。
对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"love"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。
StringBuffer/StringBuffer
通过继承AbstractStringBuilder来创建对象,默认大小为16。AbstractStringBuilder中采用一个char数组来保存需要append的字符串,char数组有一个初始大小(16),当append的字符串长度超过当前char数组容量时,则对char数组进行动态扩展,也即重新申请一段更大的内存空间,然后将当前char数组拷贝到新的位置。
18. Servlet的生命周期?
HttpServlet中用三个方法定义了servlet的生命周期,init,service,destroy,init和destroy方法只会执行一次,分别是初始化和容器销毁。
大致分为4部:Servlet类加载–>实例化–>服务–>销毁
1、Web Client向Servlet容器(Tomcat)发出Http请求。
2、Servlet容器接收Client端的请求。
3、Servlet容器创建一个HttpRequest对象,将Client的请求信息封装到这个对象中。
4、Servlet创建一个HttpResponse对象。
5、Servlet调用HttpServlet对象的service方法,把HttpRequest对象和HttpResponse对象作为参数传递给HttpServlet对象中。
6、HttpServlet调用HttpRequest对象的方法,获取Http请求,并进行相应处理。
7、处理完成HttpServlet调用HttpResponse对象的方法,返回响应数据。
8、Servlet容器把HttpServlet的响应结果传回客户端。
其中的3个方法说明了Servlet的生命周期:
init():servlet的初始化方法,只在创建servlet实例时候调用一次,Servlet是单例的,整个服务器就只创建一个同类型Servlet
service():servlet的处理请求方法,在servle被请求时,会被马上调用,每处理一次请求,就会被调用一次。ServletRequest类为请求类,ServletResponse类为响应类
destory():servlet销毁之前执行的方法,只执行一次,用于释放servlet占有的资源,通常Servlet是没什么可要释放的,所以该方法一般都是空的。
扩展内容
1)HTTP请求类型
OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送’*’的请求来测试服务器的功能。
HEAD:向服务器索要与GET请求相一致的响应,只不过响应body将不会返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取不包含在响应消息头中的元信息。
GET:向特定的资源发出请求。
POST:向特定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求body中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
PUT:向指定资源位置上传其最新内容。
DELETE:请求服务器删除所表示的资源。
TRACE:回显服务器收到的请求,主要用于测试或诊断。
2)get和post的区别
- 本质:get用于请求资源,post用于处理资源
- get方式提交,传递的参数在url中有显示,可以明显看到参数,数据不够安全,因此传输数据的长度有限制,有的浏览器为2048个字符。然而post则是隐式传递,是不能查看到传递的参数。
- 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。这个规则是看浏览器的,有些是合并发送,有些是分开发送
19. 如何用Java分配一段连续的1G的内存空间?需要注意些什么?
(1)ByteBuffer.allocateDirect(1024*1024*1024);
(2)还有一种方式是使用unsafe类:分配的是堆外内存,因此使用完成后需要自己对内存进行回收
部分源码如下
public final class Unsafe {
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
//其余部分省略 ...
}
可以看到, Unsafe
类是一个单例类, 通过静态的getUnsafe()
方法获取实例. getUnsafe()
方法中有一个权限检查的逻辑, 即: 如果不是系统域下的类调用getUnsafe()
方法将抛出SecurityException
异常.
因此, 非系统类库要获取Unsafe
的实例就不能直接调用getUnsafe()
方法. 从上面可以看到, Unsafe
类有一个类型为其本身的静态常量theUnsafe
, 因此我们可以用反射来获取其实例. 代码如下:
private static void init() {
try {
unsafeClass = Class.forName("sun.misc.Unsafe");
Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe");
boolean orignialAccessible = theUnsafeField.isAccessible();
theUnsafeField.setAccessible(true);
unsafeInstance = theUnsafeField.get(null); //unsafeInstance就是Unsafe的实例
theUnsafeField.setAccessible(orignialAccessible);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
内存管理相应方法:
-
long allocateMemory(long bytes)
开辟一块内存. 参数为要开辟内存的大小, 多少字节; 返回值是一个native pointer
, 就是内存地址. -
void freeMemory(long address)
释放一块内存. 参数为内存地址. 即allocateMemory()
方法的返回值. -
long reallocateMemory(long address, long bytes)
重新分配 (扩展) 一块内存 (之前分配内存当然会被GC回收掉). 第一个参数为原内存地址, 第二个参数为重新分配的内存的大小 (要超过前一块内存的大小), 返回新的内存地址. 原内存中的内容会迁移到新开辟的内存中.
内存操作 (往内存中存数据 或 从内存中取数据):
-
void putType(long address, type data)
、type getType(long address, long offset)
i.type
为原始数据类型:boolean
,byte
、char
、short
、int
、long
、float
、double
ii. 第一个参数address
, 为内存地址(参考allocateMemory()
)
iii. 第二个参数offset
, 字段的偏移量, 用下列方法获取: -
long objectFieldOffset(Field field)
- 获取实例字段的偏移量 -
long staticFieldOffset(Field field)
- 获取静态字段的偏移量
典型应用
DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
下图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。
那么如何通过构建垃圾回收追踪对象Cleaner实现堆外内存释放呢?
Cleaner继承自Java四大引用类型之一的虚引用PhantomReference(众所周知,无法通过虚引用获取与之关联的对象实例,且当对象仅被虚引用引用时,在任何发生GC的时候,其均可被回收),通常PhantomReference与引用队列ReferenceQueue结合使用,可以实现虚引用关联对象被垃圾回收时能够进行系统通知、资源清理等功能。如下图所示,当某个被Cleaner引用的对象将被回收时,JVM垃圾收集器会将此对象的引用放入到对象引用中的pending链表中,等待Reference-Handler进行相关处理。其中,Reference-Handler为一个拥有最高优先级的守护线程,会循环不断的处理pending链表中的对象引用,执行Cleaner的clean方法进行相关清理工作。
所以当DirectByteBuffer仅被Cleaner引用(即为虚引用)时,其可以在任意GC时段被回收。当DirectByteBuffer实例对象被回收时,在Reference-Handler线程操作中,会调用Cleaner的clean方法根据创建Cleaner时传入的Deallocator来进行堆外内存的释放。
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
20. Java有自己的内存回收机制,但为什么还存在内存泄露的问题呢?
1)首先,要理解什么是内存泄漏。
内存泄漏:无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费
例如:以下代码中的对象o尽管已经置为null,但是不会被回收,最简单的做法是将v=null
Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
2)然后,了解一下引起内存泄漏的根本原因。
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。
3)最后,要怎么预防内存泄漏呢?
通常一些成员变量引用其他对象,初始化的时候需要置空。同时,在确认一个对象无用后,将其所有引用显式的置为null;
https://www.jianshu.com/p/54b5da7c6816
21. Java里面用对象作为Key需要注意些什么? 如何实现hashcode?
(1)需要重写hashcode和equals方法。
(2)为什么要重写这两个方法呢?
get和put时都会根据以下代码进行插入和获取。所以要重写hashCode和equals方法,否则,判断结果不正确就会导致数据插入和获取失败!
p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))
put方法中,会检查该对象对应key的hashcode值在table中是否已经存在,如果存在则判断key是否相等(用到equals方法),若以上两个条件同时满足时,新的value会覆盖原来的值。如果不重写这两个方法,那么put时,可能会出现添加key相同的两个数据时,后一个数据没有覆盖前一个数据的情况。
get方法中,不重写的话,根据key会取不到对应value.
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
(3)为什么会出现hashcode呢?
之前我们比较两个值是否相等,都是利用的equals方法,在数据量少的时候不会有问题,但是数据有几万条呢?效率明显下降了,这时候hashcode出现了,集合中添加一个对象时,先调用该对象的hashcode的方法,根据相关运算得到该元素在集合中的位置,那么判断对象是否相等时,可以直接判断hashcode值是否相等,如果不相等,那么两个对象一定是不相等的,但是hashcode值相等,并不能证明两个对象相等(比如hashmap中的hash冲突),还要用equals方法判断两个对象是否相等。
(4)如何重写hashcode方法?
一般用用于相等性比较的那些字段的子集,还需要考虑到hash碰撞的问题。
《Effective Java》中提出了一种简单通用的hashCode算法
A、初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17
;
B、选取equals方法中用于比较的所有域(之所以只选择equals()中使用的域,是为了保证上述原则的第1条),然后针对每个域的属性进行计算
C、最后,把每个域的散列码合并到对象的哈希码中。
22. 简单介绍反射机制?
使用的前提条件?
- 必须先得到代表字节码的Class,Class类用于表示.class文件(字节码)
什么是反射机制?
- java反射是在运行状态中,对任意一个类,都能够知道这个类的所有属性和方法,对于任何一个对象,都能够调用它的任意一个属性和方法,这种动态获取信息以及动态调用对象的方法的功能称为java的反射机制。
Class是反射的基石
- 1.Class是一个类,一个描述类的类,封装了描述方法的Method,描述字段的Filed,描述构造器的Constructor等属性,通过反射可以得到类的各个成分。
- 2.对于每个类而言,JRE都为其保留一个不变的Class类型的对象,一个Class对象包含了特定某个类的有关信息。
- 3.Class对象只能由JVM创建
- 4.一个类在JVM中只有一个Class实例
- 5.反射相关的类:java.lang.reflect包下
应用
- 使用JDBC连接数据库的时候,都使用的反射,一般都是通过配置文件配置连接哪个数据库,比如:mysql、oracle等,以及对应的关键信息
Class.forName("com.mysql.cj.jdbc.Driver");
其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。
class Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
...
}
简单来说:
- 反射就是把各种java类中的各种成分(成员变量、方法、构造方法、构造方法、包等信息)映射成一个个的java对象
核心类:
- Class:代表一个类
- Constructor类:代表类的构造方法
- Filed类:代表类的成员变量
- Method:代表类的方法
获取Class对象的四种方式,区别是什么?
背景:类加载的过程
加载:通过累的全限定名获取二进制字节流,将二进制字节流转换成方法区中的运行时数据结构,在内存中生成Java.lang.class对象;
链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
校验:检查导入类或接口的二进制数据的正确性;(文件格式验证,元数据验证,字节码验证,符号引用验证)
准备:给类的静态变量分配并初始化存储空间;
解析:将常量池中的符号引用转成直接引用;
初始化:激活类的静态变量的初始化Java代码和静态Java代码块,并初始化程序员设置的变量值。
- 1.Object.getClass(); 通过已知对象获取 ,返回运行时真正所指的对象,完成了类的初始化
- 2.任何数据类型(包括基本数据类型)都有一个静态的Class属性 通过类名.Class,只装入类,没有进行链接步骤,所以没有初始化
- 3.通过Class的静态方法: Class.forName(String calssName):最常用,装入类并做类的初始化。Class.forName(className)方法,内部实际调用的方法是 Class.forName(className,true,classloader);第2个boolean参数表示类是否需要初始化, Class.forName(className)默认是需要初始化。
- 4.classloader:内部实际调用的方法是 ClassLoader.loadClass(className,false);第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面背景可知,不进行链接意味着不进行包括初始化等一些列步骤,那么静态块和静态对象就不会得到执行,只有在newinstance才会去执行static块;classloader只负责第一步加载,而调用clinit方法,执行static方法以及给static变量赋值都是在初始化阶段完成的。
public static void main(String[] args) {
String wholeNameLine = "cn.topcheer.office.JsonTest";
Class<?> demo = null;
ClassLoader loader = ClassLoader.getSystemClassLoader();
try {
demo = loader.loadClass(wholeNameLine);
//demo = ClassForNameAndClassLoaderTest.class.getClassLoader().loadClass(wholeNameLine);//这个也是可以的
System.out.println("demo " + demo.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//若下面这个try catch 注释掉则不会打印 类中静态代码块
try {
//是因为上面可能会异常,demo可能会是null,所以直接demo.newInstance()可能会空指针异常
//测试什么时候执行类中的静态代码
JsonTest line = (JsonTest) (demo != null ? demo.newInstance() : null);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
测试代码
public class Person {
public Person(){
System.out.println("构造函数");
}
static {
System.out.println("Initializing Person");
}
public static String string = "static param initialize";
private String name = "Alfira";
public void getName() {
System.out.println(name);
}
public void setName(String name, int a) {
this.name = name + a;
}
}
private static void show(String name) {
try {
// 返回运行时真正所指的对象
Person p = new Person();
Class classType = p.getClass();
System.out.println("执行初始化了吗?");
Method getMethod = classType.getMethod("getName", new Class[] {});
Class[] parameterTypes = {String.class, int.class};
Method setMethod = classType.getMethod("setName", parameterTypes);
getMethod.invoke(p);
setMethod.invoke(p, "Setting new ", 1);
getMethod.invoke(p);
System.out.println("实例化对象.getClass()----end\n");
// 装入类,并做类的初始化
Class classType2 = Class.forName(name);
Method getMethod2 = classType2.getMethod("getName", new Class[] {});
Class[] parameterTypes2 = {String.class, int.class};
Method setMethod2 = classType2.getMethod("setName", parameterTypes2);
System.out.println("执行初始化了吗?");
// 实例化对象
Object obj2 = classType2.newInstance();
// 通过实例化后的对象调用方法
getMethod2.invoke(obj2);
setMethod2.invoke(obj2, "Setting new ", 2); // 设置
getMethod2.invoke(obj2);
System.out.println("Class.forName----end\n");
// JVM将使用类A的类装载器,将类A装入内存(前提是:类A还没有装入内存),不对类A做类的初始化工作
Class classType3 = Person.class;
Method getMethod3 = classType3.getMethod("getName", new Class[] {});
Class[] parameterTypes3 = {String.class, int.class};
Method setMethod3 = classType3.getMethod("setName", parameterTypes3);
System.out.println("执行初始化了吗?");
// 实例化对象,因为这一句才会输出“静态初始化”以及“初始化”
Object obj3 = classType3.newInstance();
getMethod3.invoke(obj3);
setMethod3.invoke(obj3, "Setting new ", 3); // 设置
getMethod3.invoke(obj3);
System.out.println("Person.class----end");
} catch (Exception e) {
System.out.println(e);
}
}
//运行结果
//注意以上代码要分开运行,否则,静态代码块只执行一次,下面的看不到结果
Initializing Person
构造函数
执行初始化了吗?
Alfira
Setting new 1
实例化对象.getClass()----end
Initializing Person
执行初始化了吗?
构造函数
Alfira
Setting new 2
Class.forName----end
执行初始化了吗?
Initializing Person
构造函数
Alfira
Setting new 3
Person.class----end
23. ArrayList 是否会发生数组越界?
- 在并发 add 的情况下,会发生数组越界
- 这是因为在 Add 方法中有两步操作,一个是 ensureCapacityInternal(size + 1)确保数组容量如果不够
则进行扩容,一个是 elementData[size++] = e,将 size 值自增并将值 e 填入到数组当中 - 当数组中的位置仅剩一个的时候,线程 1 和线程 2 同时进入 add 方法中并都执行到了第一步 ensureCapacityInternal 操作,
- 这时候两个线程都认为数组中有位置可以填入,此时线程 1 填入后,size 加 1。然后线程 2 填入元素的时候就会数组越界。
- 因为之前当数组中的元素仅剩 1 个的时候,并没有进行扩容操作。
24. 什么是迭代器?
迭代器是一种设计模式,提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。 Java中的Iterator提供了统一遍历操作集合元素的统一接口, Collection接口实现Iterable接口, 每个集合都通过实现Iterable接口中iterator()方法返回Iterator接口的实例, 然后对集合的元素进行迭代操作. 有一点需要注意的是:在迭代元素的时候不能通过集合的方法删除元素, 否则会抛出ConcurrentModificationException 异常. 但是可以通过Iterator接口中的remove()方法进行删除.
Iterator 和 ListIterator 的区别是什么?
Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引,等等。
快速失败(fail-fast)和安全失败(fail-safe)的区别是什么? 为什么会并发修改失败?
先这两种机制都是对于迭代器而言的。 一:快速失败(fail—fast) 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改), 则会抛出Concurrent Modification Exception。 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果 内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount 变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值 刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个 异常只建议用于检测并发修改的bug。 场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。 二:安全失败(fail—safe) 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。 缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容, 即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
25. JAVA8 的 ConcurrentHashMap 为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
- 加入多个分段锁浪费内存空间。
- 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
- 为了提高 GC 的效率
JDK8 新的做法:
( 源码保留了 segment 代码,但并没有使用)put 首先通过 hash 找到对应链表过后, 查看链表是否已经有数据,如果没有, 直接用 cas 原则插入,无需加锁。如果链表已有有数据,则直接用链表第一个 元素加锁,这里加的锁是 synchronized, 虽然效率不如 ReentrantLock,但节约了空间,这里会一直用第一个 元素为锁, 直到重新计算 map 大小,比如扩容或者操作了第一个 元素为止。
★ConcurrentHashMap(JDK1.8)为什么要使用 synchronized 而不是如 ReentranLock 这样的可重入锁?
- 减少内存开销
假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,
只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。 - 获得 JVM 的支持
可重入锁毕竟是 API 这个级别的,后续的性能优化空间很小。synchronized 则是 JVM 直接支持的,JVM 能够在运行时作出相应的优化措施:
锁粗化、锁消除、锁自旋等等。这就使得 synchronized 能够随着 JDK 版本的升级而不改动代码的前提下获得性能上的提升。
26. Java 中的队列都有哪些,有什么区别。
双端队列:
LinkedList : 实现了Deque接口,受限的队列
未实现阻塞接口的:
PriorityQueue : 优先队列,本质维护一个有序列表。可自然排序亦可传递 comparator构造函数实现自定义排序。
ConcurrentLinkedQueue:基于链表 线程安全的队列。增加删除O(1) 查找O(n)
实现阻塞接口的:
实现blockqueue接口的五个阻塞队列,其特点:线程阻塞时,不是直接添加或者删除元素,而是等到有空间或者元素时,才进行操作。
ArrayBlockingQueue: 基于数组的有界队列
LinkedBlockingQueue: 基于链表的无界队列
ProiporityBlockingQueue: 基于优先次序的无界队列
DelayQueue: 基于时间优先级的队列
SynchronousQueue: 内部没有容器的队列 较特别 –其独有的线程一一配对通信机制
其中的操作有异常,null 或者 false、阻塞等区别:
方法 | 用途 | 状况 |
add | 增加一个元索 | 如果队列已满,则抛出一个 IIIegaISlabEepeplian 异常 |
remove | 移除并返回队列头部的元素 | 如果队列为空,则抛出一个 NoSuchElementException 异常 |
element | 返回队列头部的元素 | 如果队列为空,则抛出一个 NoSuchElementException 异常 |
offer | 添加一个元素并返回 true | 如果队列已满,则返回 false |
poll | 移除并返问队列头部的元素 | 如果队列为空,则返回 null |
peek | 返回队列头部的元素 | 如果队列为空,则返回 null |
put | 添加一个元素 | 如果队列满,则阻塞 |
take | 移除并返回队列头部的元素 | 如果队列为空,则阻塞 |
27. 下面哪个流类属于面向字符的输入流( )
A BufferedWriter B FileInputStream C ObjectInputStream D InputStreamReader
答案:D
java的IO操作中有面向字节(Byte)和面向字符(Character)两种方式。
面向字节的操作为以8位为单位对二进制的数据进行操作,对数据不进行转换,这些类都是InputStream和OutputStream的子类。
面向字符的操作为以字符为单位对数据进行操作,在读的时候将二进制数据转为字符,在写的时候将字符转为二进制数据,这些类都是Reader和Writer的子类。
总结:以InputStream(输入)/OutputStream(输出)为后缀的是字节流;
以Reader(输入)/Writer(输出)为后缀的是字符流。
扩展:Java流类图结构,
28.接口和抽象类的区别
a.接口:
(1)接口用于描述系统对外提供的所有服务,因此接口中的成员常量和方法都必须是公开(public)类型的,确保外部使用者能访问它们;
(2)接口仅仅描述系统能做什么,但不指明如何去做,所以接口中的方法都是抽象(abstract)方法;
(3)接口不涉及和任何具体实例相关的细节,因此接口没有构造方法,不能被实例化,没有实例变量,只有静态(static)变量;
(4)接口的中的变量是所有实现类共有的,既然共有,肯定是不变的东西,因为变化的东西也不能够算共有。所以变量是不可变(final)类型,也就是常量了。
(5) 接口中不可以定义变量?如果接口可以定义变量,但是接口中的方法又都是抽象的,在接口中无法通过行为来修改属性。有的人会说了,没有关系,可以通过 实现接口的对象的行为来修改接口中的属性。这当然没有问题,但是考虑这样的情况。如果接口 A 中有一个public 访问权限的静态变量 a。按照 Java 的语义,我们可以不通过实现接口的对象来访问变量 a,通过 A.a = xxx; 就可以改变接口中的变量 a 的值了。正如抽象类中是可以这样做的,那么实现接口 A 的所有对象也都会自动拥有这一改变后的 a 的值了,也就是说一个地方改变了 a,所有这些对象中 a 的值也都跟着变了。这和抽象类有什么区别呢,怎么体现接口更高的抽象级别呢,怎么体现接口提供的统一的协议呢,那还要接口这种抽象来做什么呢?所以接口中 不能出现变量,如果有变量,就和接口提供的统一的抽象这种思想是抵触的。所以接口中的属性必然是常量,只能读不能改,这样才能为实现接口的对象提供一个统 一的属性。
通俗的讲,你认为是要变化的东西,就放在你自己的实现中,不能放在接口中去,接口只是对一类事物的属性和行为更高层次的抽象。对修改关闭,对扩展(不同的实现 implements)开放,接口是对开闭原则的一种体现。
所以:接口的方法默认是public abstract;
public static final
注意:final和abstract不能同时出现。
java8中新增:接口中可以由默认方法和静态方法
b.抽象类:
如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。
包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。
(1)抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
(2)因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。
(3)如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。
c.区别:
(1)抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
(2)设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计
(3)方法:接口内的方法修饰符只能是public abstract,抽象类可以由有普通的成员方法;
(4)变量:接口中的成员变量只能是public static final类型的,抽象类中的成员变量可以是各种类型的;
(5)继承:一个类却可以实现多个接口,一个类只能继承一个抽象类。
29. ArrayList list = new ArrayList(20);中的list扩充几次(0)次
ArrayList的默认长度是10个,所以如果你要往list里添加20个元素肯定要扩充两次(扩充为原来的1.5倍),但是这里显示指明了需要多少空间,所以就一次性为你分配这么多空间,也就是不需要扩充了。
如果是直接add的话,应该要扩容2次,ArrayList是采取延迟分配对象数组大小空间的,当第一次添加元素时才会分配10个对象空间,当添加第11个元素的时候,会扩容1.5倍,当添加到16个元素的时候扩容为15*1.5=22,以此类推。
30. 加密算法
常用的对称加密算法有:DES、3DES、RC2、RC4、AES
常用的非对称加密算法有:RSA、DSA、ECC
使用单向散列函数的加密算法:MD5、SHA
31.指出下列程序运行的结果 ( good and gbc)
public class Example {
String str = new String("good");
char[] ch = { 'a', 'b', 'c' };
public static void main(String args[]) {
Example ex = new Example();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.print(ex.ch);
}
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'g';
}
}
java中只有值传递,当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个副本。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。
a.传递值的数据类型:八种基本数据类型和String(这样理解可以,但是事实上String也是传递的地址,只是string对象和其他对象是不同的,string对象是不能被改变的,内容改变就会产生新对象。那么StringBuffer就可以了,但只是改变其内容。不能改变外部变量所指向的内存地址)。
b.传递地址值的数据类型:除String以外的所有复合数据类型,包括数组、类和接口
注意以上两个都是值,所以是都是值传递
32. 阅读Shape和Circle两个类的定义。在序列化一个Circle的对象circle到文件时,下面哪个字段会被保存到文件中? ( radius )
class Shape {
public String name;
}
class Circle extends Shape implements Serializable{
private float radius;
transient int color;
public static String type = "Circle";
}
(1)如果类实现了Serilizable接口,那么类的所有属性和方法都会自动序列化。但实际上,有些属性不需要序列化,字段上就会加上transient字段就不会序列化了,也就是说加上transient字段的属性只在内存够中,不会序列化到磁盘。
(2)静态变量不能序列化,因为静态变量是属于类的,而序列化是相对于对象的
33. 子类没有显示调用父类构造函数,不管子类构造函数是否带参数都默认调用父类无参的构造函数,若父类没有则编译出错。
34. 下面程序的运行结果是什么()
class HelloA {
public HelloA() {
System.out.println("HelloA");
}
{ System.out.println("I'm A class"); }
static { System.out.println("static A"); }
}
public class HelloB extends HelloA {
public HelloB() {
System.out.println("HelloB");
}
{ System.out.println("I'm B class"); }
static { System.out.println("static B"); }
public static void main(String[] args) {
new HelloB();
}
}
答案
static A static B I'm A class HelloA I'm B class HelloB
对象的初始化顺序:(1)类加载之后,按从上到下(从父类到子类)执行被static修饰的语句;(2)当static语句执行完之后,再执行main方法;(3)如果有语句new了自身的对象,将从父类到子类执行构造代码块、构造器(两者可以说绑定在一起)
35. 指出下面程序的运行结果。
class A {
static {
System.out.print("1");
}
public A() {
System.out.print("2");
}
}
class B extends A{
static {
System.out.print("a");
}
public B() {
System.out.print("b");
}
}
public class Hello {
public static void main(String[] args) {
A ab = new B();
ab = new B();
}
}
执行结果:1a2b2b。创建对象时构造器的调用顺序是:先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。
36.内部类的作用
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。
而通常的内部类需要在外部类实例化后才能实例化,其语法看起来挺诡异的。
注意:Java中非静态内部类对象的创建要依赖其外部类对象,静态方法中没有this,也就是说没有所谓的外部类对象,因此无法创建内部类对象,如果要在静态方法中创建内部类对象,可以这样做:new Outer().new Inner();
37.String s = new String(“xyz”);创建了几个字符串对象?
答:两个对象,一个是静态区的”xyz”,一个是用new创建在堆上的对象。