面试题总结

四:查找链表的倒数第 K 个节点

为了能够只遍历一次就能找到倒数第k个节点,可以定义两个指针:

(1)第一个指针从链表的头指针开始遍历向前走k-1,第二个指针保持不动;

(2)从第k步开始,第二个指针也开始从链表的头指针开始遍历;

(3)由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走 在后面的)指针正好是倒数第k个结点。

注意:剑指 offer 中有提到,有提到,有提到,,,(ps:忘了)

02.jpg

六:进程在内存中是如何分区的

进程内存区域:

一:代码区:代码块是用来存放可执行文件的操作指令(存放函数的二进制代码),也就是说它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改操作)----它是不可写的。

二:全局(静态)区包含下面两个区:

(1)数据区:数据段用来存放可执行文件中已经初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。

(2)BSS区:BSS段包含了程序中未初始化全局变量。

三:常量区:常量存储区,这是一块比较特殊的区域,存放的是常量。

四:堆区:堆是由程序员分配和释放,用于存放进程运行中被动态分配的内存段,它大小并不固定,可以动态扩张或缩减。当进程调用 alloc 等函数分配内存时,新分配的内存就被动态添加到堆上;当利用release释放内存的时候,被释放的内存从堆中被剔除。

五:栈区:由编译器自动分配并释放,用户存放程序临时创建的局部变量,存放函数的参数值,局部变量等。也就是说我们函数括弧“{ }”中定义的变量(不包括static声明的变量)。除此意外在函数被调用的时候,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存。是一个临时数据寄存,交换的内存区。

九:HashMap 实现原理,hash值是如何映射到数组下标的?

HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

既然是一个数组,就有下标,HashMap的数组下标是如何实现的呢,源码是这么写的:

// hash码函数
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// table下标计算
n = tab.length;
Node p = tab[i = (n - 1) & hash];

即数组的下标 index 是 (length-1) 与 key 的hash值的 & (与) 运算。

十一:handle如果我想取消发送的消息怎么办?

tips: 只有延时消息才可以被取消,即时消息无法被取消。

handler发消息方式:
发送即时消息:
Message msg = new Message();
msg.what = 100;
handler.sendMessage(msg);
延时5秒发送消息:
Message msg = new Message();
msg.what = 100;
handler.sendMessageDelayed(msg, 5000);
当执行handler.sengMessageDelayed后,需要取消消息的发送,可以执行handler.removeMessages(100).

removeMessages会将handler对应message queue里的消息清空,如果带了int参数则是对应的消息清空。队列里面没有消息则handler会不工作,但不表示handler会停止。当队列中有新的消息进来以后handler还是会处理。

十三:如何保证数据在多线程下的访问安全?

解决办法:(1)同步代码块

       (2)同步方法

       (3)Lock 对象锁(本身是一个接口,使用其子类)

public class MoneyRunnableImp implements Runnable {


        private int sumMoney = 1000;
        @Override
        public void run() {

        while (true) {  
            /**
            * 同步代码块实现数据安全:
            * 
            * 这里面的this就是一把锁,使用这个类创建的线程使用同一把锁
            * 
            */
            synchronized (this) {
                if (sumMoney > 0) {

                    /**
                    * sumMoney = sumMoney - 1; 放在前面就不会有问题
                    */
                    // sumMoney = sumMoney - 1;
                    System.out.println(Thread.currentThread().getName() + "获得一元钱");
                    if (Thread.currentThread().getName().equals("张三")) {
                        Data.zsMoney++;
                    } else if (Thread.currentThread().getName().equals("李四")) {
                        Data.lsMoney++;
                    } else {
                        Data.wwMoney++;
                    }

                /**
                * sumMoney = sumMoney - 1; 放在后面就会出现数据安全问题(线程安全问题)
                */
                sumMoney = sumMoney - 1;
                } else {
                    System.out.println("钱分完了:");
                    System.out.println("张三获得了:" + Data.zsMoney);
                    System.out.println("李四获得了:" + Data.lsMoney);
                    System.out.println("王五获得了:" + Data.wwMoney);
                    System.out.println("他们一共获得了:" + (Data.zsMoney + Data.wwMoney + Data.lsMoney));
                try {
                    // 防止数据刷的过快,休眠一段时间
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

       }
    }

}

就是在进入同步代码块之前,必须要拿到this这个锁,当时当其他线程正常执行时,即便丢失cpu执行权,也不释放this这个锁,所以,其他线程无法执行,必须等待该线程执行完同步代码块,把锁释放了,其他的线程才可以拿着这个锁进入同步代码块。

问题来了上述代码中synchronized中的同步锁是谁?
对于非static方法同步锁就是this

对于static方法,我们使用当前方法所在类的字节码对象(当前类名.class)

tips : tips :

Java中的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。

synchronized 关键字主要有以下几种用法:

  • 非静态方法的同步;
  • 静态方法的同步;
  • 代码块。

对象锁
非静态方法使用 synchronized 修饰的写法,修饰实例方法时,锁定的是当前对象:

public synchronized void test(){
    // TODO
}

代码块使用 synchronized 修饰的写法,使用代码块,如果传入的参数是 this,那么锁定的也是当前的对象:

public void test(){
synchronized (this) {
    // TODO
}

下面通过例子来说明对象锁:

定义一个类,方法如下,将 count 自减,从 5 到 0:

public class TestSynchronized {

    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

---------------------

测试方法调用如下:

public class Run {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        thread1.start();
        thread2.start();

    }

}

---------------------

两个线程 thread1 和 thread2,同时访问对象的方法,由于该方法是 synchronized 关键字修饰的,那么这两个线程都需要获得该对象锁,一个获得后另一个线程必须等待。所以我们可以猜测运行结果应该是,一个线程执行完毕后,另一个线程才开始执行,运行例子,输出打印结果如下:

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

---------------------

本例对于对象锁进行了基础的解释。但是对象锁的范围是怎样的,对象的某个同步方法被一个线程访问后,其他线程能不能访问该对象的其他同步方法,以及是否可以访问对象的其他非同步方法呢,下面对两种进行验证:
对两个同步方法两个线程的验证:

修改类如下,加入 minus2() 方法,和 minus() 方法一样:

package com.test.run;

public class TestSynchronized {

    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public synchronized void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用如下,两个线程访问不同的方法:

public class Run {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus2();
            }
        });

        thread1.start();
        thread2.start();
    }

}

输出结果如下:

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

可以看到,某个线程得到了对象锁之后,该对象的其他同步方法是锁定的,其他线程是无法访问的。

下面看是否能访问非同步方法:

public class TestSynchronized {

    public synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用的类不变:

public class Run2 {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus2();
            }
        });

        thread1.start();
        thread2.start();

    }

}

运行结果:

Thread-1 - 4
Thread-0 - 4
Thread-1 - 3
Thread-0 - 3
Thread-1 - 2
Thread-0 - 2
Thread-1 - 1
Thread-0 - 1
Thread-1 - 0
Thread-0 - 0

可以看到,结果是交替的,说明线程是交替执行的,说明如果某个线程得到了对象锁,但是另一个线程还是可以访问没有进行同步的方法或者代码。进行了同步的方法(加锁方法)和没有进行同步的方法(普通方法)是互不影响的,一个线程进入了同步方法,得到了对象锁,其他线程还是可以访问那些没有同步的方法(普通方法)。当获取到与对象关联的内置锁时,并不能阻止其他线程访问该对象,当某个线程获得对象的锁之后,只能阻止其他线程获得同一个锁。

类锁

类锁需要 synchronized 来修饰静态 static 方法,写法如下:

public static synchronized void test(){
        // TODO
    }

或者使用代码块,需要引用当前的类:

public static void test(){
        synchronized (TestSynchronized.class) {
            // TODO
        }
    }

举例:

public class TestSynchronized {

    public static synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试类:

public class Run {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                TestSynchronized.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                TestSynchronized.minus();
            }
        });

        thread1.start();
        thread2.start();

    }

}

输出结果:

Thread-0 - 4
Thread-0 - 3
Thread-0 - 2
Thread-0 - 1
Thread-0 - 0
Thread-1 - 4
Thread-1 - 3
Thread-1 - 2
Thread-1 - 1
Thread-1 - 0

可以看到,类锁和对象锁其实是一样的,由于静态方法是类所有对象共用的,所以进行同步后,该静态方法的锁也是所有对象唯一的。每次只能有一个线程来访问对象的该非静态同步方法。

类锁的作用和对象锁类似,但是作用范围是否和对象锁一致呢

public class TestSynchronized {

    public static synchronized void minus() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

    public synchronized void minus2() {
        int count = 5;
        for (int i = 0; i < 5; i++) {
            count--;
            System.out.println(Thread.currentThread().getName() + " - " + count);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }

}

测试调用如下:

public class Run {

    public static void main(String[] args) {

        final TestSynchronized test = new TestSynchronized();

        Thread thread1 = new Thread(new Runnable() {

            @Override
            public void run() {
                TestSynchronized.minus();
            }
        });

        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                test.minus2();
            }
        });

        thread1.start();
        thread2.start();

    }

}

运行结果:

Thread-1 - 4
Thread-0 - 4
Thread-0 - 3
Thread-1 - 3
Thread-0 - 2
Thread-1 - 2
Thread-0 - 1
Thread-1 - 1
Thread-1 - 0
Thread-0 - 0

可以看到两个线程是交替进行的,也就是说类锁和对象锁是不一样的锁,是互相独立的

十五:如果两个函数方法名和参数相同只有返回值不同,是重载吗?

重写的原则:

1,重写方法的方法名称,列表参数必须和原方法相同,返回类型可以相同也可以是原类型的子类型(从 Java SE5开始支持)

2,重写方法不能比原方法的访问性差(权限不允许缩小)

3,重写方法不能比原方法抛出更多的异常

4,被重写的方法不能是 final 类型,因为 fianl 修饰的方法是无法被重写的

5,被重写的方法不能为 private,否则在其子类中只是重新定义了一个方法,并没有对其进行重写。

6,被重写的方法不能为 static,如果父类中的方法是静态的,而子类中的方法不是静态的,但是两个方法除了这一点外其他都满足重写条件,就会发生编译错误;反之亦然。即使父类和子类中的方法都是静态的,并且满足重写条件,但是任然不会发生重写,因为静态方法是在编译的时候把静态方法和类的引用类型进行匹配。

7,重写是发生在运行时,因为编译期间编译器不知道并且没办法确定该去调用哪个方法,JVM会在代码运行时做出决定。

方法重载的原则:

1,方法名称必须相同。

2,参数列表必须不同(个数不同或类型不同,参数类型排列顺序不同等)。

3,方法的返回类型可以相同也可以不同。

4,仅仅返回类型不同不足以成为方法的重载

5,重载是发生在编译时,因为编译器可以根据参数的类型来选择使用哪个方法。

重写和重载的不同:

1,方法重写要求参数列表必须一致,而方法重载要求参数列表必须不一致。

2,方法重写要求返回类型必须一致(或为其子类型),方法重载对此没有要求。

3,方法重写只能用于子类重写父类的方法,方法重载用于同一个类中的所有方法。

4,方法重写对方法的访问权限和抛出的异常有特殊的要求,而方法重载在这方面没有任何限制。

5,父类的一个方法只能被子类重写一次,而一个方法可以在所有的类中可以被重载多次。

6,重载是编译时多态,重写是运行时多态。

十六:static 的函数和内部类或者变量什么时候执行?

https://www.cnblogs.com/maohuidong/p/7843807.html

答:static是在类被初始化的时候执行的,tips:静态内部类不持有外部类的引用。

注意:静态内部类和非静态内部类一样,都是在被调用时才会被加载

静态内部类其实和外部类的静态变量,静态方法一样,只要被调用了都会让外部类的被加载。不过当只调用外部类的静态变量,静态方法时,是不会让静态内部类的被加载

为什么静态内部类可以不传入引用呢?

因为其本质就是针对外部类的内部类,而不是对象的内部类,不必用this来调用

首先,从静态的概念出发理解,静态修饰过后的一切物件都只与类相关,不与对象引用相关

As we known,静态变量,静态方法,静态块等都是类级别的属性,而不是单纯的对象属性。他们在类第一次被使用时被加载(记住,是一次使用,不一定是实例化)。我们可以简单得用 类名.变量 或者 类名.方法来调用它们。与调用没有被static 修饰过变量和方法不同的是:一般变量和方法是用当前对象的引用(即this)来调用的,静态的方法和变量则不需要。从一个角度上来说,它们是共享给所有对象的,不是一个角度私有。这点上,静态内部类也是一样的。


静态内部类的加载过程:

静态内部类的加载不需要依附外部类,在使用时才加载。不过在加载静态内部类的过程中也会加载外部类。

你可能感兴趣的:(面试题总结)