飞道的博客

Java基础面试(续001)

553人阅读  评论(0)

写文章之前,先聊聊最近的状态吧。最近课不多,却一刻也没有闲下来,感觉自己的每分每秒都有要做的事情。但因为自己干了许多徒劳无功的事情,所以有点丧,想出去走走,想找个人一起出去吃好吃的。翻了翻好友列表,很悲哀,好像没有能约的。害,算咯,一个人也罢。最近确实有点惨不忍睹,但,还是有收获的,比如上一次发表的一篇博客 java面试基础 ,就收获了一批粉丝。非常感谢大家的支持,让我有了继续创作的动力。有读者建议持续更新基础面试这一块的文章,我觉得可以。那就续!

1、Java 对象初始化顺序?

1、 初始化一个类,获得类资源
当使用new关键字来创建一个对象,或者首次直接通过类名调用类中的static属性和static方法的时候, 就会初始化一个类。
2、在堆内存上分配存储空间,所有的类属性和方法都会有相应的默认值
3、执行构造函数,检查是否有父类,如果有父类会先调用父类的构造函数,加载父类的静态资源。如果没有父类,执行默认值属性的赋值即方法的初始化动作。
4、计算出一个引用值(在栈内存中)

注意

  1. 静态初始化(包括静态属性和静态方法以及静态代码块)在程序运行过程中只会在 Class 对象首次加载的时候运行一次。这些资源都会放在 jvm 的方法区。方法区又叫静态区,跟堆一样,被所有的线程共享。方法区中包含的都是在整个程序中永远唯一的元素,包含所有的 class 和 static 变量。
  2. 因为子类的静态初始化可能会依赖于父类的静态资源,所以要先加载父类的静态资源。
  3. 因为子类 的非静态变量和方法的初始化有可能使用到其父类的属性或方法,所以子类构造默认的属性和方法之后不应该进行赋值,而要跳转到父类的构造方法完成父类对象的构造之后,才来对自己的属性和方法进行初始化。

2、什么是序列化,如何实现序列化?

简单的说,序列化是一种实现对象读写的操作。通过序列化,可以将对象进行读入和写出,实现对象的传输。

序列化:将对象转换成字节流的过程。
反序列化:将字节流转换成相应对象的过程。

具体实现

当一个对象要被序列化,首先这个类得实现 Serializable 接口,实现这个接口,就只是象征着它能被序列化。如果一个普通的类,没有实现 Serializable 接口,那么它是不可能被序列化的。

对象序列化的用途

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  • 在网络上传送对象的字节序列。

注意

  • 某个类可以被序列化,则其子类也可以被序列化
  • 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
    反序列化读取序列化对象的顺序要保持一致

3、进程和线程有什么区别?

讲到线程,首先推荐一篇文章,挺详细,感兴趣的读者可以看看。Java多线程详解

进程
是指一个内存中运行的运用程序,每个进程都有一个独立的内存空间。
线程

  • 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程中至少有一个线程。
  • 线程实际上是在进程的基础上的进一步划分,一个进程启动后,里面的若干执行路径又可以划分成若干个线程。

关系

一个线程可以创建和撤销另一个线程。同一个进程中的多个线程之间可以并发执行。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。

区别

  • 一个程序至少有一个进程,一个进程至少有一个线程。
  • 线程的划分尺度小于进程,使得多线程程序的并发性高。
  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。

4、java 当中如何实现线程?

  1. 继承Thread
  2. 实现Runnable
  3. Callable接口的使用

实现Runnable的优势:

  • 通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况
  • 可以避免单继承所带来的局限性
  • 任务与线程本身是分离的,提高了程序的健壮性

继承Thread优势:

可直接通过匿名类使用一个线程。

5、线程的生命周期?

1、初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2、运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3、阻塞(BLOCKED):表示线程阻塞于锁。
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
4、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
5、终止(TERMINATED):表示该线程已经执行完毕。

6、多线程并发或线程安全问题如何解决?

利用可以解决线程安全问题。锁又分为隐式锁和显式锁,其中隐式锁又有同步代码块和同步方法这两种解决方法。

隐式锁:线程同步:使用synochronized关键字实现自动上锁
1、同步代码块。使用锁对象,任何对象都可以作为锁对象。多个线程必须共享同一个锁对象。
2、同步方法
显示锁:使用Lock子类ReentrantLock(需要手动上锁和解锁)

7、synchronized 和 ReentrantLock 的区别?

1、是否需要手动解锁;synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
2、synchronized 是JVM层面的锁,是Java关键字。ReentrantLock是Lock的子类接口。
3、Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程一直等待下去,不能响应中断。
4、synchronized为非公平锁。 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
5、synchronized不能绑定Condition; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
6、synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

8、hashMap实现原理

JDK1.8 以前HashMap的实现是 数组+链表

JDK1.8 开始HashMap的实现是 数组+链表+红黑树。

HashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。

HashMap中默认的初始容量为16,默认的加载因子为0.75

  • 初始容量代表了哈希表中桶的初始数量,即 Entry< K,V>[] table 数组的初始长度
  • 加载因子是哈希表在其容量自动增加之前可以达到多满的一种饱和度百分比,其衡量了一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
  • 比如加载因子是0.75,当数组的使用的长度超过该数组长度*0.75的时候,该数组便会自动扩容。table在HashMap扩容的时候,长度会翻倍

所有的数据都是通过一个Node节点进行封装,其中Node节点中封装了hash值,key,value,和next指针。hash是通过key计算出的hashCode值进行对数组容量减一求余得到的(官方的求余方式是通过&运算进行的)。不同的key计算出来的hash值可能相同,解决冲突是通过拉链法(链表和红黑树)进行处理。正是因为这种存储形势,所以HashMap的存取顺序是无序的。

在JDK1.8之后,HashMap底层是由“数组+链表+红黑树”组成
当要存储一个键值对时,就会先计算键的哈希值(hashCode),然后对其进行取余操作。比如这个数组的长度为16,某个键的哈希值为17,那么17%16=1,所以这个键值对应该存在数组下标为1的位置。那有一个问题,假如某个键的哈希值的33,33%16也是等于1的,那这样子该怎么存呢。其实数组中的每个元素都是一个链表,也就是数组的某个位置可以存放多个值。当链表中的数据量大于8时,链表就会自动转换成红黑树,当红黑树中的数据量小于6时,红黑树又会自动转换成链表。如下图:

HashMap的具体实现步骤如下:

1、通过调用put(K key, V value), 利用hash(key)计算哈希值(存储位置)。
2、判断Node数组table是否为null或等于0

  • 如果是,调用resize()方法对数组进行扩容。
  • 如果不是,判断table[i]是否已经有元素。
    1)没有元素。new Node直接插入
    2)有元素。判断传入的key是否存在。
    a、若存在,则用新值覆盖旧值。
    b、不存在,判断table[i]是否为treeNode(红黑树节点)
    - a)是,红黑树直接插入。
    - b)不是,遍历链表,判断长度是否大于等于8
    - 是,转为红黑树插入
    - 不是,转为链表插入。key存在则覆盖value。

9、GC回收原理

在java中,程序员是不需要去自己释放一些内存,由虚拟机自行回收(执行),在jvm有一个垃圾回收线程,它的优先级是低优先级,在正常情况下不会执行,只有在虚拟机空闲的时候或者当前内存不足的时候执行,它会扫描没有被任何引用的对象,并将它们添加到要回收的集合当中,进行回收。

GC (Garbage Collection)的基本原理:将内存中不再被使用的对象进行回收,GC中用于回收的方法称为收集器,由于GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。
Java将堆内存分为3大部分:新生代、老年代和永久代,其中新生代又进一步划分为Eden、S0、S1(Survivor)三个区,比例为8:1:1。在程序中new出来的对象一般情况下都会在新生代里的Eden区里面分配空间,如果存活时间足够长将会进入Survivor区,进而如果存活时间再长,还会被提升分配到老年代里面。永久代代里面存放的是Class类元数据、方法描述等

垃圾收集的方法:

1、复制算法(新生代)

为了解决效率问题,复制算法将可用内存分为相等的两个部分,然后每次只使用其中的一块,当一块内存使用完,就将还存活的对象复制到另一块内存上,然后一次性清除完一块内存,然后再将第二块上的对象复制到第一块上。
会有的问题:
内存的代价太大了,基本上每次都要浪费一半的内存。
复制算法改进:
内存区域不再是1:1划分,而是将内存分为8:1:1三个部分,较大部分叫做Eden区,其余两块较小的叫做Survior区,每次都会优先用Eden区域,如果Eden区域满了,那就将对象复制到第二块内存上,然后清除Eden区,如果此时存活的对象太多,以至于Survior不够时,会将这些对象通过分配担保机制复制到老年代当中。

2、标记-清除

这是垃圾收集算法中比较基础的,根据名字就可以知道,标记那些对象需要回收,然后统一回收,这种方法很简单,
会有的问题:
1、效率不高,标记清除效率都比较低。
2、会产生大量的不连续的内存碎片,导致以后在程序中,分配较大的对象时,由于没有充足的连续内存而提前触发GC

3、标记-整理

这个算法主要是为了解决标记-清除,产生的大量内存碎片的问题,当对象存活率较高时,也解决了复制算法的效率问题,他的不同之处就是清除对象的时候,可以讲可回收的对象移动到另一端,然后清除掉边界以外的对象,这样就不会产生内存碎片。

4、分代收集

现在的虚拟机大多采用这个方式,它是根据对象的生存周期,将堆分为新生代,和老年代,在新生代中由于对象生存期较短,每次回收都会有大量的对象需要被回收。那这个时候就采用复制算法,老年代里的对象存活率比较高,没有额外的空间进行分配担保,所以可以使用标记-整理,或者标记-清除。

10、今日算法:约瑟夫问题

约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3。

解法1:使用数组实现

public class Task006 {
   

	public static void main(String[] args) {
   
		System.out.println("请输入总人数N");
		Scanner input = new Scanner(System.in);
		//获取小组的人数
		int num = input.nextInt();
		System.out.println("请输入被杀掉的是第M个人");
		//另外记录小组的总人数
		int num1=num;
		int M = input.nextInt();
		int[] nums = new int[num];
		//每个人都有一个相同的初始值1
		for(int i=0;i<nums.length;i++) {
   
			nums[i] = 1;
		}
		//开始报数n
		int n = 0;
		//数组下标
		int index = 0;
		
		while(num>1) {
   
			if(nums[index]==1) {
   
				n++;
			}
			if(n==M) {
   
				//报数3,赋值0表示淘汰
				nums[index]=0;
				//从头开始报数
				n = 0;
				//人数减1
				num--;
			}
			//数组下标加1
			index++;
			//遍历完所有人,数组下标置0,表示重新开始遍历
			if(index == num1) {
   
			    index = 0;
			   }
			
		}
		
		//遍历
		for(int i=0;i<num1;i++) {
   
			if(nums[i]==1) {
   
				System.out.println("最后剩下的一个人是第:"+(i+1)+"个人 ");
			}
		}
	
	}

}

解法2:使用环形单链表

链表节点:

public class CNode {
   
    private int data;
    private CNode next;

    public CNode(int data) {
   
        this.data = data;
    }

    public int getData() {
   
        return data;
    }

    public void setData(int data) {
   
        this.data = data;
    }

    public CNode getNext() {
   
        return next;
    }

    public void setNext(CNode next) {
   
        this.next = next;
    }
    
}

链表实现:

public class CLinkedList {
   
    private CNode head = null;

    public CLinkedList() {
   
    }

    public CNode getHead() {
   
        return head;
    }

    public void setHead(CNode head) {
   
        this.head = head;
    }

    /**
     * 解决约瑟夫问题
     * @param start 表示从第几个数字开始数数
     * @param num 表示数到第几个数字时,环形单链表的第几个节点应该出圈
     * @param sums 表示该环形单向链表中一共有多少个节点
     */
    public void countOut(int start,int num,int sums){
   
        if (head == null || start < 1 || start > sums){
   
            System.out.println("输入的参数有问题!");
            return;
        }
        CNode last = head;
        while (true){
   
            if (last.getNext()==head){
   
                break;
            }
            last = last.getNext();
        }
        for (int i = 0;i<start - 1;i++){
   
            head = head.getNext();
            last = last.getNext();
        }

        while (true){
   
            if (last == head){
   
                break;
            }
            for (int i = 0;i < num - 1; i++){
   
               head = head.getNext();
               last = last.getNext();
            }
            System.out.printf("节点:%d,出圈\n",head.getData());
            head = head.getNext();
            last.setNext(head);
        }

        System.out.printf("最终剩下的节点编号:%d",head.getData());


    }
    /**
     * 遍历环形单向链表
     */
    public void list(){
   
        if (head == null){
   
            System.out.println("该环形链表为空!");
            return;
        }else{
   
            CNode temp = head;
            while (true){
   
                System.out.printf("环形节点:%d\n",temp.getData());
                temp = temp.getNext();
                if (temp == head){
   
                    break;
                }

            }
        }

    }
    /**
     * 往环形单向链表中加入数据
     *
     * @param num
     */
    public void addCNode(int num) {
   
        if (num<1){
   
            System.out.println("num值不合理!");
            return;
        }
        CNode temp = null;
        for (int i = 1; i <= num; i++) {
   
            CNode cNode = new CNode(i);
            if (i == 1) {
   
                head = cNode;
                head.setNext(cNode);
                temp = head;
            } else {
   
                temp.setNext(cNode);
                cNode.setNext(head);
                temp = cNode;
            }
        }
    }
}

测试:

public class CLinkedListDemo {
   
    public static void main(String[] args) {
   
        CLinkedList cLinkedList = new CLinkedList();
        cLinkedList.addCNode(6);
        cLinkedList.list();
        cLinkedList.countOut(1,5,6);
    }
}

结果:


转载:https://blog.csdn.net/weixin_44881854/article/details/115709784
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场