Java面试经典题目

1.线程中start()和run()的区别
a.调用start方法将会创建一个新线程,并且在run方法中的代码将会在新线程上运行。启动一个线程实际是请求Java虚拟机运行相应的线程,而这个线程何时能够运行是由线程调度器决定的。start调用结束并不表示相应线程已经开始运行,这个线程可能稍后运行,也可能永远也不会运行。
b.直接调用run方法时,程序并不会创建新线程,run方法内部的代码将在当前线程上运行。只是调用一个实例的方法而已,并未启动线程。
大多数情况下调用run方法是一个defect或者失误。

2.一道神奇的List遍历及移除元素题
上代码

	public static void main(String[] args){
		List<String> myCollection=new ArrayList<String>(10);
		myCollection.add("111");
		myCollection.add("222");
		Iterator it=myCollection.iterator();
		int index=0;
		while(it.hasNext()){
			String value=it.next().toString();
			System.out.println(value);
			if(index++>0){
				myCollection.remove(value);
//				it.remove();
			}
		}
	}

乍一看,代码没啥毛病。一运行,报了异常

111
222
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
	at java.util.ArrayList$Itr.next(ArrayList.java:831)
	at com.shi.sync006.Test.main(Test.java:15)

抛出异常的是iterator.next()。看下Iterator next方法实现源码

public E next() {
	checkForComodification();
	int i = cursor;
	if (i >= size)
		throw new NoSuchElementException();
	Object[] elementData = ArrayList.this.elementData;
	if (i >= elementData.length)
		throw new ConcurrentModificationException();
	cursor = i + 1;
	return (E) elementData[lastRet = i];
}
final void checkForComodification() {
	if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	}
}

在next方法中首先调用了checkForComodification方法,该方法会判断modCount是否等于expectedModCount,不等于就会抛出java.util.ConcurrentModificationExcepiton异常。
我们接下来跟踪看一下modCount和expectedModCount的赋值和修改。
在创建Iterator的时候会将modCount赋值给expectedModCount,在遍历ArrayList过程中,没有其他地方可以设置expectedModCount了,因此遍历过程中expectedModCount会一直保持初始值2(调用add方法添加了2个元素,修改了2次)。遍历的时候是不会触发modCount自增的,但是执行了一次remove(),这行代码执行后modCount++变为了3,但此时的expectedModCount仍然为2。在执行next方法时,遇到modCount != expectedModCount方法,导致抛出异常java.util.ConcurrentModificationException。
dubug执行代码如图,当执行remove后,cursor依然是2,但是size变为1了,
在这里插入图片描述
由于cursor和size不相等,导致本该退出while循环的却没退出while循环,致使再次执行了next,出现报错。

        public boolean hasNext() {
            return cursor != size;
        }

这么做是为了阻止程序员在不允许修改的时候修改对象,起到保护作用,避免出现未知异常。

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 
Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变。当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
在iterator.remove()方法中,同样调用了ArrayList自身的remove方法,但是调用完之后并非就return了,而是expectedModCount = modCount重置了expectedModCount值,使二者的值继续保持相等。

在这里插入图片描述

List<String> myCollection=new ArrayList<String>(10);

下面分析这段代码,源码如下,

    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = {};
    public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

ArrayList有参构造的参数initialCapacity就是集合的初始容量。如果我们在实例化有参构造时赋的参数大于0。 就是把初始容量赋成你传入的值了。ArryList的无参构造是把初始容量设置成了EMPTY_ELEMENTDATA也就是10。
Vectors数据增长是100% ArrayList 是50%
3.Java的IO四大基类
a.从内存的角度来看
– 输入流:只能从中读取数据,而不能向其写入数据
– 输出流:只能向其写入数据,而不能从中读取数据
java的输入流主要由 InputStream 和 Reader作为基类,而输出流主要以 OutputStream 和 Writer作为基类,这些基类无法创建实例。
b. 字节流和字符流
字节流和字符流的区别在于所操作的数据单元不同,字节流操作的数据单元是8位的字节,字符流操作的数据单元是16位的字符。
字节流主要由 InputStream 和 OutputStream作为基类,字符流主要有 Reader 和 Writer作为基类。

4.程序,进程,线程的概念
程序:为了让计算机执行某些操作或解决某个问题而编写一系列有序的指令集合。
进程:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程:是操作系统进程中能够独立执行的实体(控制流),是处理器调度和分派的基本单位。 可以看成是进程内的多条执行路径。
5.对象的创建的五种方式
a.使用new关键字:这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们还可以调用任意的构造函数(无参的和有参的)。比如:Student student = new Student();
b.使用Class类的newInstance方法:我们也可以使用Class类的newInstance方法创建对象,这个newInstance方法调用无参的构造器创建对象,如:Student student2 = (Student)Class.forName(“根路径.Student”).newInstance(); 或者:Student stu = Student.class.newInstance();
c.使用Constructor类的newInstance方法:本方法和Class类的newInstance方法很像,java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象。我们可以通过这个newInstance方法调用有参数的和私有的构造函数。如: Person p=Person.class.getConstructor().newInstance(); 这两种newInstance的方法就是大家所说的反射,事实上Class的newInstance方法内部调用Constructor的newInstance方法。这也是众多框架Spring、Hibernate、Struts等使用后者的原因。
d.使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去,用clone方法创建对象并不会调用任何构造函数。要使用clone方法,我们必须先实现Cloneable接口并实现其定义的clone方法。如:Student stu2 = (Student)stu.clone();这也是原型模式的应用。
e.使用反序列化:当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。
6.为什么要进行垃圾回收?
随着程序的运行,内存中存在的实例对象、变量等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至会因为可用内存不足造成一些不必要的系统异常。
优点: 垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。
大对象直接进入老年代,避免大对象分配内存时由于分配担保机制带来的复制而降低效率。
7.Java 中都有哪些引用类型?
强引用:发生 gc 的时候不会被回收。
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
弱引用:有用但不是必须的对象,在下一次GC时会被回收。
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用PhantomReference实现虚引用,虚引用的用途是在gc时返回一个通知。
8.怎么判断对象是否可以被回收?
一般有两种方法来判断:
引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
可达性分析算法:从 GC Roots 作为起点,引用链作为路径。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
9.JVM垃圾回收算法
a.标记清除
首先标记出所有需要回收的对象,在标记完成后统一回收掉被标记的对象 (老年代)
优点:实现简单,不需要对象进行移动。
缺点:第一个是执行效率不稳定,第二个是内存空间的碎片化问题
b.标记复制
将可用内存按容量划分为大小相等的两块,每次使用其中的一块,这一块用完了就将还存活者的对象复制到另一块上面 (新生代)
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点:内存缩小到原来的一半.
不过新生代的98%的对象熬不过第一轮收集. 所以eden区和幸存区大小比例是8:1
c.标记整理
让所有存活的对象都移向内存空间的一端,然后直接清理掉边界以外的内存 (老年代.)
优点:解决了标记-清理算法存在的内存碎片问题。
缺点:仍需要进行局部对象移动,一定程度上降低了效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值