最近需要面临找工作的压力,所以在寒假的时候恶补了一下关于android方面的知识,这是一个系列的博客,希望自己可以坚持更新下去。
今天找了一些Java基础的面试题,我在里面挑选了一些我还有些模糊的题,在此处记录一下:
1.面向对象的特征:
(1)抽象:将一类对象的共同特征总结出来构造类的过程。
(2)继承:从已有类得到继承信息,创建新类的过程。
(3)封装:把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
(4)多态:允许不同子类型的对象对同一消息做出不同的响应。
2.float f=3.4是否正确?
这是一个比较经典的问题了,答案相信大家都已经知道了,不正确,当然的解释就是3.4是双精度数,也就是double型,将double转换为float需要强转,也就是float f=(float)3.4;当时我看到解释后还是很懵逼,我就想知道,为啥3.4就是double型呢?后来我才明白,java对于一个小数的初始默认类型就是double型,所以一切都明白了;
3.A. short s1=1;s1=s1+1;与B. short s1=1;s1+=1;的区别
这一题只要弄清楚两点,第一就是,1是int型;第二就是,s1+=1相当于s1=(short)(s1+1);这样就知道,A式是错误的,s1+1是int型的,赋值给s1需要强转;B式相应就是正确的了;
4.Java中的内存管理机制:
Java中把内存大致分为三个部分:
(1)方法区:也叫静态存储区,在这里存储的是直接书写的常量,静态数据,全局变量,在这里的数据在整个程序运行时一直都会存在。
(2)栈区:方法体内的局部变量,基本数据类型的变量,对象的引用等;
(3)堆区:也叫动态内存分配,new 和 构造器创建的对象保存在这里;
举个例子: String str=new String(“hello”);
str这个引用存储在栈上,new出来的字符串对象存储在堆上,hello这个字符串存储在方法区内;
5.一些容易被忽略的小点:
(1)Math.round(11.5)=12; Math.round(-11.5)=-11; 向大值取整;
(2)如果两个对象 x,y满足x.equals(y)=true;那么他们的哈希码也应该相同;
(3)String是final类,无法被继承;
(4)Java中方法调用仅支持值传递;
6.String ,StringBuilder和StringBuffer的区别:
(1)String为只读字符串,String引用的字符串内容是不能改变的,StringBuffer和StringBuilder的值可以直接修改;
(2)StringBulider与StringBuffer的方法完全一样,但是StringBulider的所有方法没有被Synchronized修饰,所以是单线程,是线程不安全的,但是其效率最高;
(3)String赋予新值会重新开辟内存地址,而StringBuffer和StringBuilder则采用append和insert等方法来改变字符串的值,是在原有对象的内存上进行操作,所以大量字符串拼接时采用StringBuffer或StringBuilder,少量可采用String的“+”;
(4)这里还有一个比较偏的点:请说出下面程序的输出:
1 2 3 4 5 6 7 8 9 10 11 |
class StringEqualTest { public static void main(String[] args) { String s1 = "Programming"; String s2 = new String("Programming"); String s3 = "Program" + "ming"; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s1.intern()); } } |
正确答案是:false, true ,true;
第一个很好理解,s1和s2的引用不一样,当然是false
第二个我也很纳闷为什么会是true,原来啊,String的+被Java进行了特殊处理,在运算过程中会使用StringBuilder或者StringBuffer代替求和,而这两个在进行字符串的运算时,不会改变其内存地址,内存中已经存在了Programming这个字符串,那么就不会重新开辟内存地址,所以是true;
第三个要知道intern方法的使用,String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用。
7.重载(OverLoad)和重写(Override)的区别:
这个大部分人应该都清楚,这里我总结一下:
(1)重载实现的是编译时的多态,重写实现的运行时的多态,所谓的编译时的多态就是在编译时就能确定执行的是哪个多态方法,否则就是运行时的多态。
(2)重载发生在一个类中,同名方法具有不同的参数列表(参数类型不同,参数个数不同),重写发生在子类与父类中,子类被重写的方法与父类被重写的方法具有相同的返回类型,比父类要更好访问,不能比父类有更多的异常
这里有一个经典的面试题,为什么函数重载不能根据返回类型区分?
答案:因为函数调用时不会指定类型信息,编译器不知道你到底要调用那个方法。
8.描述一下JVM加载class文件的原理机制:
这个问题设计的东西比较多,在回答这个问题前,我们要知道一些知识点:
(1)Java的类加载器:
A.BootStrop:根加载器,负责加载JVM基础核心类库;
B.Extension:扩展类加载器,从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
C.System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
(2)由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
A.加载:把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。
B.连接:经过加载后的Class对象还不可用,进入连接阶段,此阶段又分为三个步骤:
a.验证:检查二进制字节码是否正确合格;
b.准备:给类的类变量分配内存并初始化;
c.解析:将常量池的符号引用转换为直接引用;
C.初始化:最后一步对类进行初始化,遵循“先父后子”,“先静态后普通”的原则,依次执行初始化语句。
9.如何实现对象的克隆?
(1)实现cloneable接口并重写clone()方法;(浅克隆,只能复制值类型的成员变量,引用类型的无法复制)
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
}
}
(2)实现Serializable接口;(深克隆,真正的实现对象克隆)
public class Outer implements Serializable{
private static final long serialVersionUID = 369285298572941L; //最好是显式声明ID
public Inner inner;
//Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化]
public Outer myclone() {
Outer outer = null;
try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 将流序列化成对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
outer = (Outer) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return outer;
}
}
public class Inner implements Serializable{
private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
public String name = "";
public Inner(String name) {
this.name = name;
}
@Override
public String toString() {
return "Inner的name值为:" + name;
}
}
10.将GB2312编码的字符串转换为ISO-8859-1编码的字符串:
String s1=“你好”;
String s2=new String(s1,getBytes("GB2312","ISO-8859-1"));
11.ArrayList,Vector,LinkedList的存储性能和特性:
(1)ArrayList和Vector底层都是使用数组作为存储数据的方式,具有索引速度快,增删速度慢的特点,且由于Vector内部使用了Synchronized关键字,所以性能会较差;
(2)LinkedList底层是双向链表,具有查询索引速度慢,插入删除速度快的特点,且为线程不安全的;
12.TreeMap与TreeSet中比较元素的方法:
TreeSet要求对象类必须实现Comparable接口,提供了CompareTo()方法;
TreeMap要求存放的键值对中的键必须实现Comparable接口;
13.Sleep()与yield()的区别:
(1)sleep()给其他线程运行机会不会考虑线程的优先级,yield()只会给相同优先级或者更高的优先级的线程以运行机会;
(2)sleep()后转入阻塞状态,yield()后转入就绪状态;
(3)sleep()抛出InterruptedException异常,而yield()不会;
(4)sleep()比yield()具有更好地移植性;
14.线程池的有关知识:
(1)核心类:ThreadPoolExecutor类;
(2)构造方法:public ThreadPoolExecutor(int corePoolsize,int maxnumpoolsize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedException handler);
(3)7大参数:
A.corePoolsize:核心线程数;
B.maxnumpoolsize:最大线程数;
C.keepAliveTime:保持活跃的时间,线程无任务时多长时间被终止;
D.TimeUnit:时间单位;
E.workQueue:阻塞队列,一般使用LinkedBlockingQueue和SynchronizeQueue
F.threadFactory:线程工厂,创建线程;
G.handler:表示拒绝处理任务是的策略;
(4)关键方法:
A.execute():提交任务时执行;
B.submit():可返回任务执行结果的提交任务的方法;
C.shutdown()/shutdownNow():关闭线程池;
(5) 四大线程池:
A.newCachedThreadPool:可缓存线程池,可回收空闲线程;
B.newFixedThreadPool:定长线程池,控制最大并发数;
C.newScheduleThreadPool:定长定时周期性线程池;
D.newSingleThreadPool:单线程线程池;
例子一个:
public static class ThreadPool { private int corePoolSize;//核心线程数 private int maximumPoolSize;//最大线程数 private long keepAliveTime;//休息时间 private ThreadPool(int corePoolSize,int maximumPoolSize,long keepAliveTime) { this.corePoolSize=corePoolSize; this.maximumPoolSize=maximumPoolSize; this.keepAliveTime=keepAliveTime; } public void execute(Runnable r) { if (executor==null){ executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } executor.execute(r); } public void cancle(Runnable r){ if (executor!=null){ //从线程队列中移除对象 executor.getQueue().remove(r); } } }
15.GC是什么?为什么要有GC?
GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。
补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
- 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
- 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
- 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。
(1)Lock可完成Synchronize的所有功能
(2)Lock有比Synchronized更精确的线程语义和更好地性能,不强制一定要获得锁,lock必须手动释放锁;
(3)Synchronized是Java内置的关键字,而Lock是java的一个接口类;
(4)Synchronized在发生异常时会自动释放锁,而lock必须手动释放锁;
(5)Lock可以让等待锁的线程相应中断,而Synchronized无法办到;
(6)Lock可以知道是否获取到了锁,而Synchronized无法办到;
(7)Lock可以提高多线程进行读操作的效率,而Synchronized不可以;
17.编程实现文件拷贝:
(1)public static void fileCopy(String source,String target) throws IOException{
try(InputStream in=new FileInputStream(source)){
try(OutputStream out=new FileOutputStream(target)){
byte[] bs=new byte[4096];
int count=0;
while((count=in.read(bs))!=-1){
out.write(bs, 0, count);
}
}
}
}
Java NIO实现:
(2)public static void fileCopyNIO(String source,String target) throws IOException{
try(FileInputStream in=new FileInputStream(source)){
try(FileOutputStream out=new FileOutputStream(target)){
FileChannel inChannel=in.getChannel();
FileChannel outChannel=out.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(4096);
while(inChannel.read(buffer)!=-1){
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
}
}
}
18.统计字符串在文件中出现的次数:
public static int countWordInFile(String fileName,String word) throws IOException {
int count=0;
File file=new File(fileName);
try(FileReader reader=new FileReader(file)){
try(BufferedReader bufferedReader=new BufferedReader(reader)){
String line=null;
while((line=bufferedReader.readLine())!=null){
int index=-1;
while((line.length()>word.length())&&(index=line.indexOf(word))>0){
count++;
line=line.substring(index+word.length());
}
}
}
}
return count;
}
19.列出目录所有文件:
public static void showDirectory(String pathname) {
File file=new File(pathname);
for(File temp:file.listFiles()){
if (temp.isFile()) {
System.out.println(temp.getName());
}
}
}
20.关于网络编程:用Java的套接字编程实现一个多线程的回显(echo)服务器。
服务器端:
中心思想:在主线程中开启一个死循环,死循环中开启一个待命的线程,用于接收客户端发送过来的消息;
public class SocketServer {
public static void main(String[] args) {
try(ServerSocket serverSocket=new ServerSocket(6789)){
System.out.println("服务器已开启...");
while(true){
Socket socket=serverSocket.accept();
new Thread(new ClientHandler(socket)).start();
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
private static class ClientHandler implements Runnable{
private Socket client;
public ClientHandler(Socket client) {
this.client=client;
}
@Override
public void run() {
// TODO Auto-generated method stub
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(client.getInputStream()))){
PrintWriter pWriter=new PrintWriter(client.getOutputStream());
String msg=bufferedReader.readLine();
System.out.println(msg+"由"+client.getInetAddress()+"发出");
pWriter.println(msg);
pWriter.flush();
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
try {
client.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
客户端:
中心思想:通过socket.getOutputStream()得到写出字符流,将消息发送出去;
public class ScoketClient {
public static void main(String[] args) throws IOException{
Socket socket=new Socket("localhost", 6789);
Scanner scanner=new Scanner(System.in);
System.out.println("请输入发送的信息");
String msg=scanner.nextLine();
scanner.close();
PrintWriter pWriter=new PrintWriter(socket.getOutputStream());
pWriter.println(msg);
pWriter.flush();
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("收到:"+br.readLine());
socket.close();
}
}
21.正则表达式:
\b:匹配单词的开头和结尾,单词分界处;
\w:匹配所有字母或数字或下划线或汉字;
\d:匹配所有数字;
\s:匹配空白符;
^:表示单词的开始;$:表示单词结束;
.:除了换行符以外的任意字符;
*:前面的内容可以连续重复使用任意次以使整个表达式得到匹配;
{2}:连续重复匹配2次;
?:匹配0次或1次;
+:匹配1次或更多;
(?=exp):匹配exp前面的位置;
(?<=exp):匹配exp后面的位置;
一个栗子:
如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import java.util.regex.Matcher; import java.util.regex.Pattern; class RegExpTest { public static void main(String[] args) { String str = "北京市(朝阳区)(西城区)(海淀区)"; Pattern p = Pattern.compile(".*?(?=\\()"); Matcher m = p.matcher(str); if(m.find()) { System.out.println(m.group()); } } } |
中心思想:
可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
public class ReflectionUtils {private ReflectionUtils() {
throw new AssertionError();
}
public static Object getValue(Object target,String fieldName) {
Class<?> clazz=target.getClass();
String[] fs=fieldName.split("\\.");
try{
for(int i=0;i<fs.length-1;i++){
Field field=clazz.getDeclaredField(fs[i]);
field.setAccessible(true);
target=field.get(target);
clazz=target.getClass();
}
Field f =clazz.getDeclaredField(fs[fs.length-1]);
f.setAccessible(true);
return f.get(target);
}catch (Exception e) {
throw new RuntimeException();
}
}
public static void setValue(Object target,String fieldName,Object value) {
Class<?> clazz=target.getClass();
String[] fs=fieldName.split("\\.");
try {
for(int i=0;i<fs.length-1;i++){
Field field=clazz.getDeclaredField(fs[i]);
Object val=field.get(target);
field.setAccessible(true);
if (val==null) {
Constructor<?> constructor=field.getType().getDeclaredConstructor();
constructor.setAccessible(true);
val=constructor.newInstance();
field.set(target, val);
}
target=val;
clazz=field.getClass();
}
Field field=clazz.getDeclaredField(fs[fs.length-1]);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
// TODO: handle exception
}
}
public static void main(String[] args) throws Exception{
String aString="hello";
Method method=aString.getClass().getMethod("split",String.class);
System.out.println(method.invoke(aString,"e"));
}
}
23.冒泡排序:(逼格灰常高)
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
(1)先将算法封装到具有共同接口的独立的类中使得它们可以相互替换
public interface Sort {
/**
* 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换)
*/
/**
* 排序
* @param list 待排序的数组
*/
public <T extends Comparable<T>> void sort(T[] list);
/**
* 排序
* @param list 待排序的数组
* @param comp 比较两个对象的比较器
*/
public <T> void sort(T[] list, Comparator<T> comp);
}
(2)写工具类实现上述接口:
public class BubbleSorter implements Sort{
@Override
public <T extends Comparable<T>> void sort(T[] list) {
// TODO Auto-generated method stub
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (list[j].compareTo(list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
@Override
public <T> void sort(T[] list, Comparator<T> comp) {
// TODO Auto-generated method stub
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (comp.compare(list[j], list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
}
24.折半查找:
折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是O(logN)。
/*** 使用循环实现的二分查找
* @param x
* @param key
* @param comp
* @return
*/
private static <T> int binarySearch(T[] x,T key,Comparator<T> comp) {
int low=0;
int high=x.length-1;
while(low<=high){
int mid=(high-low)>>>1;
int temp=comp.compare(x[mid], key);
if (temp>0) {
high=mid-1;
}else if(temp<0){
low=mid+1;
}else {
return mid;
}
}
return -1;
}
/**
* 使用递归实现的二分查找
* @param x
* @param low
* @param high
* @param key
* @return
*/
private static <T extends Comparable<T>> int binarySearch(T[] x,int low,int high,T key){
if(low<=high){
int mid=low+((high-low)>>1);
if(key.compareTo(x[mid])==0){
return mid;
}else if(key.compareTo(x[mid])<0){
return binarySearch(x, low, mid-1, key);
}else {
return binarySearch(x, mid+1, high, key);
}
}
return -1;
}
最近需要面临找工作的压力,所以在寒假的时候恶补了一下关于android方面的知识,这是一个系列的博客,希望自己可以坚持更新下去。
今天找了一些Java基础的面试题,我在里面挑选了一些我还有些模糊的题,在此处记录一下:
1.面向对象的特征:
(1)抽象:将一类对象的共同特征总结出来构造类的过程。
(2)继承:从已有类得到继承信息,创建新类的过程。
(3)封装:把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
(4)多态:允许不同子类型的对象对同一消息做出不同的响应。
2.float f=3.4是否正确?
这是一个比较经典的问题了,答案相信大家都已经知道了,不正确,当然的解释就是3.4是双精度数,也就是double型,将double转换为float需要强转,也就是float f=(float)3.4;当时我看到解释后还是很懵逼,我就想知道,为啥3.4就是double型呢?后来我才明白,java对于一个小数的初始默认类型就是double型,所以一切都明白了;
3.A. short s1=1;s1=s1+1;与B. short s1=1;s1+=1;的区别
这一题只要弄清楚两点,第一就是,1是int型;第二就是,s1+=1相当于s1=(short)(s1+1);这样就知道,A式是错误的,s1+1是int型的,赋值给s1需要强转;B式相应就是正确的了;
4.Java中的内存管理机制:
Java中把内存大致分为三个部分:
(1)方法区:也叫静态存储区,在这里存储的是直接书写的常量,静态数据,全局变量,在这里的数据在整个程序运行时一直都会存在。
(2)栈区:方法体内的局部变量,基本数据类型的变量,对象的引用等;
(3)堆区:也叫动态内存分配,new 和 构造器创建的对象保存在这里;
举个例子: String str=new String(“hello”);
str这个引用存储在栈上,new出来的字符串对象存储在堆上,hello这个字符串存储在方法区内;
5.一些容易被忽略的小点:
(1)Math.round(11.5)=12; Math.round(-11.5)=-11; 向大值取整;
(2)如果两个对象 x,y满足x.equals(y)=true;那么他们的哈希码也应该相同;
(3)String是final类,无法被继承;
(4)Java中方法调用仅支持值传递;
6.String ,StringBuilder和StringBuffer的区别:
(1)String为只读字符串,String引用的字符串内容是不能改变的,StringBuffer和StringBuilder的值可以直接修改;
(2)StringBulider与StringBuffer的方法完全一样,但是StringBulider的所有方法没有被Synchronized修饰,所以是单线程,是线程不安全的,但是其效率最高;
(3)String赋予新值会重新开辟内存地址,而StringBuffer和StringBuilder则采用append和insert等方法来改变字符串的值,是在原有对象的内存上进行操作,所以大量字符串拼接时采用StringBuffer或StringBuilder,少量可采用String的“+”;
(4)这里还有一个比较偏的点:请说出下面程序的输出:
1 2 3 4 5 6 7 8 9 10 11 |
class StringEqualTest { public static void main(String[] args) { String s1 = "Programming"; String s2 = new String("Programming"); String s3 = "Program" + "ming"; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s1.intern()); } } |
正确答案是:false, true ,true;
第一个很好理解,s1和s2的引用不一样,当然是false
第二个我也很纳闷为什么会是true,原来啊,String的+被Java进行了特殊处理,在运算过程中会使用StringBuilder或者StringBuffer代替求和,而这两个在进行字符串的运算时,不会改变其内存地址,内存中已经存在了Programming这个字符串,那么就不会重新开辟内存地址,所以是true;
第三个要知道intern方法的使用,String对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中,然后返回常量池中字符串的引用。
7.重载(OverLoad)和重写(Override)的区别:
这个大部分人应该都清楚,这里我总结一下:
(1)重载实现的是编译时的多态,重写实现的运行时的多态,所谓的编译时的多态就是在编译时就能确定执行的是哪个多态方法,否则就是运行时的多态。
(2)重载发生在一个类中,同名方法具有不同的参数列表(参数类型不同,参数个数不同),重写发生在子类与父类中,子类被重写的方法与父类被重写的方法具有相同的返回类型,比父类要更好访问,不能比父类有更多的异常
这里有一个经典的面试题,为什么函数重载不能根据返回类型区分?
答案:因为函数调用时不会指定类型信息,编译器不知道你到底要调用那个方法。
8.描述一下JVM加载class文件的原理机制:
这个问题设计的东西比较多,在回答这个问题前,我们要知道一些知识点:
(1)Java的类加载器:
A.BootStrop:根加载器,负责加载JVM基础核心类库;
B.Extension:扩展类加载器,从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
C.System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
(2)由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
A.加载:把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。
B.连接:经过加载后的Class对象还不可用,进入连接阶段,此阶段又分为三个步骤:
a.验证:检查二进制字节码是否正确合格;
b.准备:给类的类变量分配内存并初始化;
c.解析:将常量池的符号引用转换为直接引用;
C.初始化:最后一步对类进行初始化,遵循“先父后子”,“先静态后普通”的原则,依次执行初始化语句。
9.如何实现对象的克隆?
(1)实现cloneable接口并重写clone()方法;(浅克隆,只能复制值类型的成员变量,引用类型的无法复制)
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("学生1:" + stu1.getNumber());
System.out.println("学生2:" + stu2.getNumber());
}
}
(2)实现Serializable接口;(深克隆,真正的实现对象克隆)
public class Outer implements Serializable{
private static final long serialVersionUID = 369285298572941L; //最好是显式声明ID
public Inner inner;
//Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化]
public Outer myclone() {
Outer outer = null;
try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 将流序列化成对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
outer = (Outer) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return outer;
}
}
public class Inner implements Serializable{
private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
public String name = "";
public Inner(String name) {
this.name = name;
}
@Override
public String toString() {
return "Inner的name值为:" + name;
}
}
10.将GB2312编码的字符串转换为ISO-8859-1编码的字符串:
String s1=“你好”;
String s2=new String(s1,getBytes("GB2312","ISO-8859-1"));
11.ArrayList,Vector,LinkedList的存储性能和特性:
(1)ArrayList和Vector底层都是使用数组作为存储数据的方式,具有索引速度快,增删速度慢的特点,且由于Vector内部使用了Synchronized关键字,所以性能会较差;
(2)LinkedList底层是双向链表,具有查询索引速度慢,插入删除速度快的特点,且为线程不安全的;
12.TreeMap与TreeSet中比较元素的方法:
TreeSet要求对象类必须实现Comparable接口,提供了CompareTo()方法;
TreeMap要求存放的键值对中的键必须实现Comparable接口;
13.Sleep()与yield()的区别:
(1)sleep()给其他线程运行机会不会考虑线程的优先级,yield()只会给相同优先级或者更高的优先级的线程以运行机会;
(2)sleep()后转入阻塞状态,yield()后转入就绪状态;
(3)sleep()抛出InterruptedException异常,而yield()不会;
(4)sleep()比yield()具有更好地移植性;
14.线程池的有关知识:
(1)核心类:ThreadPoolExecutor类;
(2)构造方法:public ThreadPoolExecutor(int corePoolsize,int maxnumpoolsize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedException handler);
(3)7大参数:
A.corePoolsize:核心线程数;
B.maxnumpoolsize:最大线程数;
C.keepAliveTime:保持活跃的时间,线程无任务时多长时间被终止;
D.TimeUnit:时间单位;
E.workQueue:阻塞队列,一般使用LinkedBlockingQueue和SynchronizeQueue
F.threadFactory:线程工厂,创建线程;
G.handler:表示拒绝处理任务是的策略;
(4)关键方法:
A.execute():提交任务时执行;
B.submit():可返回任务执行结果的提交任务的方法;
C.shutdown()/shutdownNow():关闭线程池;
(5) 四大线程池:
A.newCachedThreadPool:可缓存线程池,可回收空闲线程;
B.newFixedThreadPool:定长线程池,控制最大并发数;
C.newScheduleThreadPool:定长定时周期性线程池;
D.newSingleThreadPool:单线程线程池;
例子一个:
public static class ThreadPool { private int corePoolSize;//核心线程数 private int maximumPoolSize;//最大线程数 private long keepAliveTime;//休息时间 private ThreadPool(int corePoolSize,int maximumPoolSize,long keepAliveTime) { this.corePoolSize=corePoolSize; this.maximumPoolSize=maximumPoolSize; this.keepAliveTime=keepAliveTime; } public void execute(Runnable r) { if (executor==null){ executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } executor.execute(r); } public void cancle(Runnable r){ if (executor!=null){ //从线程队列中移除对象 executor.getQueue().remove(r); } } }
15.GC是什么?为什么要有GC?
GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。
补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
- 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
- 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
- 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。
(1)Lock可完成Synchronize的所有功能
(2)Lock有比Synchronized更精确的线程语义和更好地性能,不强制一定要获得锁,lock必须手动释放锁;
(3)Synchronized是Java内置的关键字,而Lock是java的一个接口类;
(4)Synchronized在发生异常时会自动释放锁,而lock必须手动释放锁;
(5)Lock可以让等待锁的线程相应中断,而Synchronized无法办到;
(6)Lock可以知道是否获取到了锁,而Synchronized无法办到;
(7)Lock可以提高多线程进行读操作的效率,而Synchronized不可以;
17.编程实现文件拷贝:
(1)public static void fileCopy(String source,String target) throws IOException{
try(InputStream in=new FileInputStream(source)){
try(OutputStream out=new FileOutputStream(target)){
byte[] bs=new byte[4096];
int count=0;
while((count=in.read(bs))!=-1){
out.write(bs, 0, count);
}
}
}
}
Java NIO实现:
(2)public static void fileCopyNIO(String source,String target) throws IOException{
try(FileInputStream in=new FileInputStream(source)){
try(FileOutputStream out=new FileOutputStream(target)){
FileChannel inChannel=in.getChannel();
FileChannel outChannel=out.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(4096);
while(inChannel.read(buffer)!=-1){
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
}
}
}
18.统计字符串在文件中出现的次数:
public static int countWordInFile(String fileName,String word) throws IOException {
int count=0;
File file=new File(fileName);
try(FileReader reader=new FileReader(file)){
try(BufferedReader bufferedReader=new BufferedReader(reader)){
String line=null;
while((line=bufferedReader.readLine())!=null){
int index=-1;
while((line.length()>word.length())&&(index=line.indexOf(word))>0){
count++;
line=line.substring(index+word.length());
}
}
}
}
return count;
}
19.列出目录所有文件:
public static void showDirectory(String pathname) {
File file=new File(pathname);
for(File temp:file.listFiles()){
if (temp.isFile()) {
System.out.println(temp.getName());
}
}
}
20.关于网络编程:用Java的套接字编程实现一个多线程的回显(echo)服务器。
服务器端:
中心思想:在主线程中开启一个死循环,死循环中开启一个待命的线程,用于接收客户端发送过来的消息;
public class SocketServer {
public static void main(String[] args) {
try(ServerSocket serverSocket=new ServerSocket(6789)){
System.out.println("服务器已开启...");
while(true){
Socket socket=serverSocket.accept();
new Thread(new ClientHandler(socket)).start();
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
private static class ClientHandler implements Runnable{
private Socket client;
public ClientHandler(Socket client) {
this.client=client;
}
@Override
public void run() {
// TODO Auto-generated method stub
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(client.getInputStream()))){
PrintWriter pWriter=new PrintWriter(client.getOutputStream());
String msg=bufferedReader.readLine();
System.out.println(msg+"由"+client.getInetAddress()+"发出");
pWriter.println(msg);
pWriter.flush();
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
try {
client.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
客户端:
中心思想:通过socket.getOutputStream()得到写出字符流,将消息发送出去;
public class ScoketClient {
public static void main(String[] args) throws IOException{
Socket socket=new Socket("localhost", 6789);
Scanner scanner=new Scanner(System.in);
System.out.println("请输入发送的信息");
String msg=scanner.nextLine();
scanner.close();
PrintWriter pWriter=new PrintWriter(socket.getOutputStream());
pWriter.println(msg);
pWriter.flush();
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("收到:"+br.readLine());
socket.close();
}
}
21.正则表达式:
\b:匹配单词的开头和结尾,单词分界处;
\w:匹配所有字母或数字或下划线或汉字;
\d:匹配所有数字;
\s:匹配空白符;
^:表示单词的开始;$:表示单词结束;
.:除了换行符以外的任意字符;
*:前面的内容可以连续重复使用任意次以使整个表达式得到匹配;
{2}:连续重复匹配2次;
?:匹配0次或1次;
+:匹配1次或更多;
(?=exp):匹配exp前面的位置;
(?<=exp):匹配exp后面的位置;
一个栗子:
如果要从字符串中截取第一个英文左括号之前的字符串,例如:北京市(朝阳区)(西城区)(海淀区),截取结果为:北京市,那么正则表达式怎么写?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import java.util.regex.Matcher; import java.util.regex.Pattern; class RegExpTest { public static void main(String[] args) { String str = "北京市(朝阳区)(西城区)(海淀区)"; Pattern p = Pattern.compile(".*?(?=\\()"); Matcher m = p.matcher(str); if(m.find()) { System.out.println(m.group()); } } } |
中心思想:
可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
public class ReflectionUtils {private ReflectionUtils() {
throw new AssertionError();
}
public static Object getValue(Object target,String fieldName) {
Class<?> clazz=target.getClass();
String[] fs=fieldName.split("\\.");
try{
for(int i=0;i<fs.length-1;i++){
Field field=clazz.getDeclaredField(fs[i]);
field.setAccessible(true);
target=field.get(target);
clazz=target.getClass();
}
Field f =clazz.getDeclaredField(fs[fs.length-1]);
f.setAccessible(true);
return f.get(target);
}catch (Exception e) {
throw new RuntimeException();
}
}
public static void setValue(Object target,String fieldName,Object value) {
Class<?> clazz=target.getClass();
String[] fs=fieldName.split("\\.");
try {
for(int i=0;i<fs.length-1;i++){
Field field=clazz.getDeclaredField(fs[i]);
Object val=field.get(target);
field.setAccessible(true);
if (val==null) {
Constructor<?> constructor=field.getType().getDeclaredConstructor();
constructor.setAccessible(true);
val=constructor.newInstance();
field.set(target, val);
}
target=val;
clazz=field.getClass();
}
Field field=clazz.getDeclaredField(fs[fs.length-1]);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
// TODO: handle exception
}
}
public static void main(String[] args) throws Exception{
String aString="hello";
Method method=aString.getClass().getMethod("split",String.class);
System.out.println(method.invoke(aString,"e"));
}
}
23.冒泡排序:(逼格灰常高)
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
(1)先将算法封装到具有共同接口的独立的类中使得它们可以相互替换
public interface Sort {
/**
* 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换)
*/
/**
* 排序
* @param list 待排序的数组
*/
public <T extends Comparable<T>> void sort(T[] list);
/**
* 排序
* @param list 待排序的数组
* @param comp 比较两个对象的比较器
*/
public <T> void sort(T[] list, Comparator<T> comp);
}
(2)写工具类实现上述接口:
public class BubbleSorter implements Sort{
@Override
public <T extends Comparable<T>> void sort(T[] list) {
// TODO Auto-generated method stub
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (list[j].compareTo(list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
@Override
public <T> void sort(T[] list, Comparator<T> comp) {
// TODO Auto-generated method stub
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (comp.compare(list[j], list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
}
24.折半查找:
折半查找,也称二分查找、二分搜索,是一种在有序数组中查找某一特定元素的搜索算法。搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组已经为空,则表示找不到指定的元素。这种搜索算法每一次比较都使搜索范围缩小一半,其时间复杂度是O(logN)。
/*** 使用循环实现的二分查找
* @param x
* @param key
* @param comp
* @return
*/
private static <T> int binarySearch(T[] x,T key,Comparator<T> comp) {
int low=0;
int high=x.length-1;
while(low<=high){
int mid=(high-low)>>>1;
int temp=comp.compare(x[mid], key);
if (temp>0) {
high=mid-1;
}else if(temp<0){
low=mid+1;
}else {
return mid;
}
}
return -1;
}
/**
* 使用递归实现的二分查找
* @param x
* @param low
* @param high
* @param key
* @return
*/
private static <T extends Comparable<T>> int binarySearch(T[] x,int low,int high,T key){
if(low<=high){
int mid=low+((high-low)>>1);
if(key.compareTo(x[mid])==0){
return mid;
}else if(key.compareTo(x[mid])<0){
return binarySearch(x, low, mid-1, key);
}else {
return binarySearch(x, mid+1, high, key);
}
}
return -1;
}