java面试
异常处理和IO操作
打断正常流程的情况分为两类,错误Error和异常Exception,这两个都是继承自Throwable类的子类。对于错误,我们一般不需要进行干预,直接终止程序。对于异常需要进行处理。
异常处理定式 try…catch…finally
除非使用exit退出程序,其他情况finally都会执行。
运行期异常不必包含在try中
- throw,throws和throwable的关系
throw抛出异常,throws写在方法声明部分参数列表面,指示方法可能抛出的异常。
throwable是公共Exception和Error的公共基类。
异常部分知识点
- finally中应该放内存回收相关代码
- 在子类中不应该抛出比父类范围更广的异常
- 尽量减少try监控区域
- 先用具体的异常类处理,最后用Exception。
- catct中尽可能处理异常。
- 出现异常后尽量保证项目不会终止。两个平行的业务其中一个出现异常不应该影响另外一个。
常见的IO读写操作
- File对象
File file = new File(path)
File flist[]=file.listFiles();
- Java中的流
- 流分为字节流和字符流
- 字节流
用来操作二进制文件,有两个接口,InputStream
和OutputStream
,实现类有FileInputStream
,BufferedInputStream
,FileOutputStream
,BufferedOutputStream
四个。 - 字符流
用来操作文本文件,有两个接口,Reader
和Writer
,实现类有FileReader
,FileWriter
,BufferedReader
,BufferedWriter
四个还有InputStreamReader,OutputStreamReader。
- 字节流
FileInputStream
和FileOutputStream
字节流
FileInputStream fileInputStream=new FileInputSteam(new File(path)); // 每次读取一个字节 int res=fileInputStream.read(); // 每次读取一个字节数组,并返回读取到的字节数量 byte[] bytes=new byte[1024]; int len=fileInputStream.read(bytes); // byte[]转String String str=new String(bytes);
InputStreamReader
传入的参数由byte[]
变成char
了,char
占两个字节。第二个参数可以传入编码BufferedReader
,需要接受一个字符流InputStreamReader
,然后调用readLine
方法。
FileInputStream fileInputStream = new FileInputStream(file); InputStreamReader bufferedReader=new InputStreamReader(inputStreamReader); BufferedReader bufferedReader=new BufferedReader(inputStreamReader); line=bufferReader.readLine();
- 如图所示
- 非阻塞性的NIO操作
NIO操作是面向缓存的,而不是面向流的。NIO的三大重要组件是Channel
、Buffer
、Selector
。
通道是双向的,流是单向的。
channel对象有4种:FileChannel
、DatagramChannel
、SocketChannel
、ServerSocketChannel
在NIO中,程序员往往会配套使用Buffered和Channel,Channel用来读写操作,Buffered用来缓存读写的内容。- 通道读写文件例子
String filepath="D:/xxxx.txt"; File file=new File(filepath); String dstpath="D:/xxxxx.txt"; File dstf=new File(dstpath); int bufferSize=1024; FileChannel src=new FileInputStream(file).getChannel(); FileChannel dst=new FileOutputStream(dstf).getChannel(); ByteBuffer buffer=ByteBuffer.allocate(bufferSize); while(src.read(buffer)!=-1){ buffer.flip(); dst.write(buffer); buffer.clear(); }
- 选择器
- 打开选择器
Selector selector=Selector.open();
- Channel和选择器使用时,需要把channel切换到非阻塞状态,
channel.configureBlocking(false);
- 将通道注册到选择器
SelectionKey key = channel.register( selector, Selectionkey.OP_READ);
不同的参数值用|
分开。如果返回对应的状态码,表示对应状态就绪。 int val=selector.select();
返回可以使用的通道数量。- 通过
selector.selectedKeys()
返回所有可以操作的通道对象。使用迭代器遍历,通过key.isReadable
、key.isWritable
判断状态。使用完后,要remove()
否则下次遍历还会遍历到。
- 打开选择器
- 流分为字节流和字符流
解析XML文件
总体来说,有两种方式,基于DOM的和基于SAX的。
- 基于DOM的
由于基于DOM的方式会将整个文件载入内存,所以可以边解析边修改。而且可以解析已经解析过的内容。 - 基于SAX的
基于回调函数的方式解析的,因此并不需要把整个文档载入内存,这样可以节省内存资源。解析速度快。适用于比较大的XML文件。
SQL,JDBC与数据库编程
-
sql语句的执行时间 = 数据库服务器执行该sql语句的时间 + 结果返回时间
-
count(*)和count(字段名)区别:当字段值为null时,count(字段名)不计数。count(字段名)比count(*)快。推荐使用count(主键)的方式。
-
insert尽量不要省略字段列表;sql server和mysql支持多条插入,oracle不支持多条插入。一次性插入条数在500到1000条之间比较好。
-
delete可以通过in语句同时删除多个记录,但是同时删除的记录数不能太多。
-
存在更新,不存在插入 merge mysql不支持,sql server,oracle支持。
merge into 表1 a using 源表 b on (a.字段1=b.字段2)
when matched then update set a.字段=b.字段
when not matched then insert into a(字段列表) values(值列表)
关于存储过程的分析
存储过程只在创建时进行编译,以后每次执行都不需要重新编译。缺点是存储过程可移植性差。难以调试发现错误。对比jdbc的批处理和存储过程,存储过程没有太大优势。
通过JDBC开发读写数据库的代码
JDBC屏蔽了数据库的实现细节和差异,给程序员提供了比较统一的调用方式。
开发步骤
-
装载驱动
Class.forName("com.mysql.jdbc.Driver")
-
创建连接
conn=DriverManager.getConnection(url,username,pwd)
-
创建statement对象:
stmt=conn.createStatement();
-
执行查询语句:
ResultSet result=stmt.executeQuery(sql);
执行删除更新插入语句:\
- 第一种
int affectRows=stmt.executeUpdate(sql)
- 第二种
ps=conn.prepareStatement(updatesql); int affectRows=ps.executeUpdate();
- 第一种
-
遍历取结果:
result.next() result.getString("id") result.getInt("字段名")
优化数据库部分代码
- 把相对固定的连接信息写入配置文件中
properties文件 一般项目中有两种环境,开发环境和生产环境
通过Properties读取properties文件代码:
Properties prop=new Properties();
InputStream in=null;
try{
in=new BufferedInputStream(new FileInputStream(path));
prop.load(in);
字段=prop.getProperty("name")
}catch(IOException e){e.printStackTrace();}
finally{
if(in!=null){
try{
in.close();
}catch(IOException e){e.printStackTrace();}
prop.clear();
}
}
用PreparedStatement以批处理的方式操作数据库
- 在sql语句中需要传参数的地方用
?
作为占位符 - 调用
PreparedStatement pstmt=conn.preparedStatement(sql);
创建对象; - 放入参数
pstmt.setString(参数序号,参数值);pstmt.setInt(参数序号,参数值)
参数序号从1开始。 - 每放入一组参数,执行
pstmt.addBatch()
进行缓存(500-1000条记录为宜) - 批处理结束,执行
pstmt.executeBatch();
C3P0连接池
代码在初始化连接池是会初始化一定量的连接对象,在程序中外部使用者获得这些连接对象并使用,使用完毕这些对象仍然保存,避免因频繁创建和释放数据库连接对象而带来的性能损耗。
ComboPooledDataSource ds=new ComboPooledDataSource();
try{
ds.setDriverClass("com.mysql.jdbc.Driver");
}catch(PropertyVetoException e){e.printStackTrace();}
ds.setJdbcUrl()
ds.setUser()
ds.setPassword()
ds.setMaxPoolSize()
ds.setMinPoolSize()
ds.setInitialPoolSize()
最大生存时间和最大空闲时间应该尽可能大,否则可能出现数据库操作过程中连接丢失。
通过下面方式获取数据库连接对象ds.getConnection()
JDBC事务
- 开启事务
- 设置是否开启事务
conn.setAutoCommit(false)
; - 提交
conn.commit()
- 回滚
conn.rollBack()
事务中的常见问题:
- 脏读 读到别的事务要回滚的数据(其他事务不可读取该事物修改未提交的值)
- 幻读 一个事务的操作导致另外一个事务中前后两次查询结果不同(添加问题 操作事务完成之前,其他事务不可添加新数据。)
- 不可重复读 一个数据的操作导致另外一个事务前后两次读到不同的数据。
事务隔离级别
- 读取未提交(脏读、幻读、不可重复读)
- 读取提交(幻读、不可重复度)
- 可重读(幻读)
- 可串行化(都可避免)
比较严格的要求,往往会牺牲性能为代价。
索引
索引分为单列索引和组合索引。
索引的实质也是一张表,该表中保存了主键和索引字段,并指向实体表的记录。
- 创建索引,
create index indexname on table(字段名(索引长度))
- 修改表结构创建索引
alter table tablename add index indexname(colname)
- 创建表的时候直接指定
create table mytable(字段列表,index indexname (字段名(长度)))
删除索引drop index indexname on tablename
- 唯一索引
create unique index indexname on tablename(字段名(长度))
alter table tablename add unique indexname (字段名(长度))
create talbe tablename( unique indexname (字段名(长度)))
查看索引
show index from tablename
反射机制和代理模式
反射机制实现的基础:Class类
当一个类被装入java虚拟机(jvm)时,便会产生一个与之关联的Class对象。通过Class.forName()我们也可以获得指定类的Class对象。
反射机制:通过java的字节码获得里面的属性方法等信息,并调用方法。
反射的常见用法:查看某个类的属性方法等信息;装载指定的类到内存;通过输入参数,调用指定的方法。
- 查看
class Student{
private int Id;
private String Name;
}
Class<Student> clazz=Student.class
Fields[] fields=clazz.getDeclaredFields();
Methods[] methods=clazz.getDeclaredMethods();
- 装载
Class<?> clazz=Class.forName("Student")
newInstance
和new
都可以得到一个可用的对象,但是newInstance
是通过Java虚拟机的类加载机制把指定的类加载到内存中。但newInstance
只能调用无参构造函数,若要使用有参数的构造函数,得用new
。
- 调用
代理模式和反射
静态代理
三种角色的定义:抽象角色、真实角色、代理角色
抽象角色是真实角色与代理角色的共同接口;真实角色是要被引用的角色;代理角色中包含真实对象的引用。
代理模式的附带优点是降低代码耦合度。使用代理模式的场景是为了更快或者更便宜的方式得到某些服务。
动态代理
通过反射实现的,在运行时动态的生成代理类。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Service {
String sellCar(String carName);
}
class ServiceImpl implements Service {
public String sellCar(String carName) {
return carName + " is ready!";
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
MyInvocationHandler(Object target){
this.target=target;
}
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("call:"+method.getName());
Object result=method.invoke(target,objects);
return result;
}
}
public class ReflectDemo {
public static void main(String[] args) {
Service service=new ServiceImpl();
InvocationHandler invocationHandler=new MyInvocationHandler(service);
Service proxy=(Service) Proxy.newProxyInstance(service.getClass().getClassLoader(),service.getClass().getInterfaces(),invocationHandler);
System.out.println(proxy.sellCar("hello"));
}
}
通过invoke方法进行调用,便于aop,可以在调用方法之前或者之后加上相关代码。
动态代理实现方法
实现InvocationHandler接口并重写invoke方法。实现含一个参数的构造函数Object类型。
多线程与并发编程
多线程实现的两种方法:
继承Thread
类:
class SimpleThread extends Thread{
public void run(){
}
}
实现Runnable
接口:
class ThreadPriority implements Runnable{
int number;
public ThreadPriority(){
}
public void run(){
}
public static void Main(String[] args){
Thread t1=new Thread(new ThreadPriority(1);
t1.setPriority(5)
t1.start();
}
}
java中优先级从高到底1-10
调用sleep必须包含在try catch中。
编程时考虑多线程,操作系统考虑多进程。
线程生命周期
- 初始化状态:使用new创建对象,没有调用start方法。
- 调用了start方法之后,可执行状态
- 阻塞状态:调用了
sleep
或者wait
- 死亡状态:调用
stop
或者destory
- 阻塞状态到可执行状态转换:调用
notify
或者notifyAll
多线程竞争和同步
sleep和wait的区别:
- sleep是线程的方法,wait是Object的方法。
sleep
不会释放lock
,但是wait
会释放lock
,而且会加入到等待队列。- sleep不依赖于同步器
synchronized
,但是wait需要依赖synchronized
关键字。 - sleep不需要被唤醒,但是wait需要。
synchronized
可以作用于方法上,放在方法名前面。
相当于给调用方法的对象加上了锁。例如o.add()
,给o
加锁后不会有其他对对象争用o;
synchronized
作用于代码块
代码块中的代码同一时间只能有一个线程访问。
一旦一个线程执行完wait方法后,会释放所有锁,然后进入阻塞状态,无法主动回到可执行状态。一定要通过其他线程的notify
或者notifyAll
唤醒。
synchronized:作用在方法上的局限是只能保证方法层面的排他性,不能保证业务层面的排他性。
ReentrantLock()可重入锁,递归锁
Lock lock=ReentrantLock();
lock.lock();
lock.unlock();
还可以使用lock.tryLock()方法加锁,如果失败返回false,否则返回true。不会无限等待。
Condition实现线程间的通信
await()让自己等待,释放相应的对象锁。
signalAll()唤醒其他线程。
Condition
对象是由Lock对象生成的,condition=lock.newCondition();
可以实现精准唤醒。
通过Semaphore
管理多线程的竞争
计数信号量。可以用来限制访问特定资源的线程数量。
- 构造函数,
public Semaphore(int permits,boolean fair)
fair为true保证先来先服务。 - 获得访问权的方法
public void acquire() throws InterruptedException
- 释放资源
public void release()
创建可重入锁,也可以指定其是否公平。
ReentrantReadWriteLock读锁和写锁。读写锁可以在保证并发时数据准确的前提下允许多个线程读。
readlock,writelock
volatile的作用
每次不是从线程内部的内存获取值,而是从主存获取值,避免创建副本。提高效率
ThreadLocal只在本线程有效
通过ThreadPoolExecutor实现线程池
ThreadPoolExecutor executor=new ThreadPoolExecutor(corePoolSize,maxPoolSize,long keepAliveTime,TimeUnit,workQueue,rejectexecutionHandler)
然后使用executor.execute(thread)执行
当线程池中线程数小于corePoolsize时,为新来的任务开启新线程;
当线程池达到corePoolSize时,新到的线程放入workQueue中;
当workQueue满且maximumPoolSize>corepoolsize时,会为新到的线程创建新的执行线程;
当线程总数超过maximumPoolSize时,启用拒绝策略;
当线程总数超过corepoolsize时,空闲时间等于keepaliveTime,关闭空闲线程。
通过callable让线程返回结果
import java.util.concurrent.*;
class Task implements Callable<Integer>{
public Integer call() throws Exception {
return 0;
}
}
public class ReflectDemo{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor= Executors.newCachedThreadPool();
Task task=new Task();
Future<Integer> result=executor.submit(task);
System.out.println(result.get());
}
}
通过Executors创建4种类型的线程池
- newCachedThreadPool 创建可缓存线程池
- newFixedThreadPool 创建定长线程池
- newScheduledThreadPool 创建定长线程池,支持周期性的任务
- newSingleThreadExecutor 创建单线程线程池
创建多线程的方法
- 通过继承
Thread
- 通过实现
Runnable
,由于java中不支持多继承,所以实现Runnable
方式创建多线程用的比较多。 - 通过实现
Callable
,线程执行结束的时候就可以返回结果了。使用Future<?>
来接收返回结果,并用get
方法取值。
从线程内存角度分析并发情况
从内存角度分析并发结果不一致的原因
如果某个线程要操作data变量,线程会先把data变量装载到线程内部的内存中做一个副本,之后线程就不再和主存内的data变量有任何关系,而是操作副本。操作完后再写回主内存。
线程不安全的集合
如果项目运行在单线程环境下,建议选择线程不安全的集合。否则,选择线程安全的集合。可以通过Collections.synchronizedList(new ArrayList())的方法把线程不安全的包装成线程安全的。
虚拟机内存优化
字节码、虚拟机、JRE、跨平台
java代码编译成字节码,运行在虚拟机上,虚拟机屏蔽了不同操作系统的差异,保证了跨平台,实现了装载字节码并执行。JRE包含java虚拟机和核心类库。
虚拟机体系结构
- 装载子系统
主要分为启动类加载器(BootstrapClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(ApplicationClassLoader)、用户自定义的类加载器(User Defined ClassLoader)
和类加载相关的两个异常:加载时找不到类ClassNotFoundException;如果加载的类引用的类找不到NoClassDefFoundExpection - 运行时数据区:方法区、堆区、栈区、程序计数器、本地方法栈模块
- 执行引擎和本地接口
- 本地接口调用操作系统本地库
优化虚拟机主要是优化堆区内存 - 静态数据、基本数据类型、引用等存放位置:
静态类型存放在方法区
new出来的对象存放在堆区
指向new出来对象的引用存放在栈区
基本数据类型数据和引用都存放在栈区。
常量类数据存放在方法区的常量池中。
java垃圾回收机制
从Eden到两个Survivor、Tenured,触发的垃圾回收都是轻量级回收,标记复制算法。
当Tenured满了后,会启动full gc,对所有区域进行回收,耗费时间较长。
启动full gc的情况:
年老代被写满;
持久带被写满
程序员显式调用System.gc(不会立即执行,而是系统在合适的时间执行。)
在执行full gc时,会导致stop the world现象。
stop the world:只有gc线程在运行,其他线程都挂起。为了保证在复制存活对象时对象的一致性。
判断对象可回收的依据
引用计数值方法:引用计数值等于0时可回收;缺点是不能回收循环引用。
根搜索算法:从gc root出发,回收不可达的对象。
可以作为gc root的对象:栈中引用对象、静态属性引用的对象、常量引用的对象、本地方法栈引用的对象。
finalize方法
如果不重写finalize方法,直接回收;否则,将finalize方法放入F-Queue队列。另一个线程会定时遍历F-Queue队列。在finalize方法执行完毕后,gc会再次判断该对象是否有强引用,如果有则复活,否则,回收。
强、弱、软、虚四种引用
强引用不会被回收
软引用如果空间够不会被回收
适合做缓存,如果空间够,则不回收,否则回收掉
弱引用会被回收
弱引用具有自动更新的功能(主要是自动删除)。
虚引用
可以实现在回收后销毁对象。
虚引用和引用队列组合使用。
在gc后可以在引用队列中找到对象。
及时释放内存
stop the world、栈溢出、内存溢出错误
stop the world内存占用量很高的情况下容易不停地stop the world,导致程序无法正常工作。
栈溢出尽量避免递归或者控制递归深度。
内存溢出错误new thread无法创建线程,new无法创建对象,堆空间不足,类加载器加载时持久区不够。
内存泄漏
- 一些数据库或IO连接没有关闭
- new了空间没有删除引用
创建list时放入5个string,然后取出来,引用2次。取出来设置为null,引用减1,但还是在list中引用,无法被释放。
内存优化具体做法
- 物理对象(connection或IO对象)用完及时close()
- 使用完对象指向null
obj=null
- 不应该频繁操作String
- 集合对象用完后及时clear()
- 合理使用软引用,弱引用。
-调整运行参数- Xms设置程序初始堆大小
- Xmx设置程序最大堆大小
(其他的一般不改)
外部类和内部类是平行的,有可能外部类比内部类更早被释放掉。所以内部类使用外部类时,参数前面要加final,放在常量池内。