java打字员面试题目总结含答案

目录

1、junit用法,before,beforeClass,after, afterClass的执行顺序

2、Nginx的请求转发算法,如何配置根据权重转发

3、用hashmap实现redis有什么问题

4、mvn install和mvn deploy的区别

5、两个Integer的引用对象传给一个swap方法在方法内部交换引用,返回后,两个引用的值是否会发现变化

6、ReenTrantLock与Synchronized的区别

7、redis的应用场景

8、redis支持的数据类型

9、redis的数据淘汰策略

10、用java实现一个简单的lru算法

11、redis的lru算法实现

12、redis中zset关于跳表的数据结构

13、redis的管道pipeline

14、transient关键字

15、进程简介及进程间通信方式

16、为什么不推荐使用Executors直接创建线程池

17、如何控制线程池线程的优先级

18、Spring如何解决循环依赖

19、事务隔离级别

20、mysql的MVCC多版本并发控制

21、Innodb的行锁

22、MyIsam和Innodb的区别?何时使用MyIsam

23、为什么说B+树比B树更适合数据库索引?

24、为什么JDBC需要打破双亲委派模型

25、Spring中BeanFactory和ApplicationContext的区别

26、CDN是什么

27、哈希冲突是什么及解决方式

28、二分查找简介

29、happens-before原则

30、java内存模型简介

31、如何保证可见性

32、UML中四大关系

33、tcp长连接和短连接的区别

34、父类的静态方法能否被子类重写

35、抽象类的意义

36、静态内部类的意义

37、谈谈解析与分派

38、String为什么要设计成不可变的

39、简单说一下泛型

40、谈谈浅拷贝和深拷贝

41、tcp可靠性传输的实现

42、聚集索引和非聚集索引

43、分布式与集群的差别

44、为什么不推荐直接使用Log4j、Logback等日志框架的API?

45、java如何打印方法的调用堆栈?

46、如何进行mysql索引优化?

47、什么是内存泄漏

48、为什么静态方法不能直接调用非静态成员(方法和变量)

49、java使用什么数据类型来表示金额


均为自己思考整理,本意只做个人记录,看到的都是缘分

1、junit用法,before,beforeClass,after, afterClass的执行顺序

@Before:初始化方法,对于每一个测试方法都要执行一次(注意与BeforeClass区别,后者是对于所有方法执行一次)
@After:释放资源,对于每一个测试方法都要执行一次(注意与AfterClass区别,后者是对于所有方法执行一次)
@Test:测试方法,在这里可以测试期望异常和超时时间 
@Test(expected=ArithmeticException.class)检查被测方法是否抛出ArithmeticException异常 
@Ignore:忽略的测试方法 
@BeforeClass:针对所有测试,只执行一次,且必须为static void,使用JUnitCore.runClasses(TestClass.class)的时候可以验证该方法只执行了一次
@AfterClass:针对所有测试,只执行一次,且必须为static void 

所以执行顺序为:@BeforeClass -> @Before -> @Test -> @After -> @AfterClass

2、Nginx的请求转发算法,如何配置根据权重转发

Nginx的负载均衡是从 upstream 模块中所定义的服务器列表选取一台用于发送请求,以达到请求转发的效果,自带的总共有4种算法:
1)轮询方式(默认方式,就是把请求平均的转发给各个服务器)
2)权重方式(通过配置weight参数实现,权重值较大的服务器收到请求的概率则较大,可以根据服务器的性能配置不同的权重配比)
3)ip_hash方式(根据客户端的ip计算出一个hash值,保证每个ip对应的请求都能访问到同一后台服务器,在一定程度上避免了session不能跨服务器的问题)
4)least_conn(最少连接数策略,把请求转发给连接数较少的服务器,适用于请求处理时间不一致导致服务器过载的情况)

3、用hashmap实现redis有什么问题

hashmap实现的是本地缓存,而redis是分布式缓存,因此使用hashmap实现redis会有如下问题:
1、redis中的缓存在重启后仍然可以生效,而hashmap是内存对象,一重启就没了
2、redis中的缓存可以被多个主机使用,即redis可以分布式部署,而hashmap只能在当前机器上使用
3、hashmap不是线程安全的,并且多线程同时调用hashmap的resize方法后,后续调用get方法时,其链表结构可能会形成闭环,然后进入死循环,可以考虑concurrentHashMap

4、mvn install和mvn deploy的区别

前者是将打包的包部署到本地maven仓库,以供其他项目使用,而后者不仅部署到本地maven仓库,同时部署到远程maven私服仓库

5、两个Integer的引用对象传给一个swap方法在方法内部交换引用,返回后,两个引用的值是否会发现变化

不会,java里方法的参数传递方式只有值传递,所谓按值传递是传递的值的拷贝,按引用传递是传递的引用的地址值,java里除了基本类型和string,其他都是按引用传递。调用方法时,实际上就是赋值操作,题目中的交换引用应该是用的'='赋值操作符,针对引用类型,只会改变引用中所保存的地址,原来的地址被覆盖掉,但是原来的对象不会被改变,除非该对象本身提供了改变自身的方法并在方法中调用

6、ReenTrantLock与Synchronized的区别

1)前者是jdk实现的锁,后者是依赖jvm实现的,由虚拟机自身进行维护,即随着jdk的升级,锁的性能也会随之上升
2)前者的颗粒度和灵活度更细,但是使用起来会略显麻烦,需手动加锁、释放锁,而后者由编译器去保证锁的获取和释放
3)前者可以指定使用公平锁或非公平锁,而后者只能使用非公平锁(所谓公平锁是指先等待的线程先获取锁)
4)前者提供了Condition类来实现分组唤醒需要唤醒的线程们,而后者要么随机唤醒一个要么唤醒全部线程
5)前者提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制

7、redis的应用场景

缓存、分布式锁、利用丰富的数据结构来实现不同的业务场景(计数器、排行榜、好友关系、简单消息队列)

8、redis支持的数据类型

字符串(string)、列表(list)、集合(set)、散列(hash)、有序集合(zset)

9、redis的数据淘汰策略

1)no-eviction(默认):禁止驱逐数据,对于写请求不再提供服务,直接返回错误(del请求和特殊请求除外)
2)volatile-lru:从设置了过期时间的key中使用lru算法进行淘汰(lru:淘汰最近最少使用)
3)volatile-ttl:从设置了过期时间的key中根据过期时间进行淘汰,越早过期的优先被淘汰
4)volatile-random:从设置了过期时间的key中随机淘汰
5)allkeys-lru:在所有key中使用lru算法进行淘汰
6)allkeys-random:在所有key中随机淘汰
ps:针对lru和ttl,挑选的数据并不是绝对的最近最少使用或过期时间最早,redis只是保证在随机挑选中的数据是满足上述条件的;当使用volatile-策略时,如果没有key可以淘汰,则和no-eviction一样返回错误

10、用java实现一个简单的lru算法

lru算法是一种缓存置换算法,如果一个数据在最近一段时间没有被使用,那么将来被使用到的可能性也很小,所以就可以淘汰掉。实现一个简单的lru算法主要就是对链表的考察,重点就是在put和get时将对应节点移动到链表头部,链表的尾部便是最近最少使用。

public class LRUCache<K, V> {

    /**
     * 支持的最大节点数
     */
    private int capacity;
    /**
     * 当前节点数
     */
    private int count;
    /**
     * 节点集合
     */
    private Map<K, Node<K, V>> nodeMap;
    /**
     * 头结点
     */
    private Node<K, V> head;
    /**
     * 尾节点
     */
    private Node<K, V> tail;

    /**
     * 初始化
     * @param capacity
     */
    public LRUCache (int capacity) {
        this.capacity = capacity;
        nodeMap = new HashMap<>();
        // 初始化头尾节点,减少判断为空的代码
        Node headNode = new Node(null, null);
        Node tailNode = new Node(null, null);
        headNode.next = tailNode;
        tailNode.pre = headNode;
        this.head = headNode;
        this.tail = tailNode;
    }

    public void put(K key, V val) {
        Node node = nodeMap.get(key);
        if (null == node) {
            // 判断是否超过最大容量
            if (count >= capacity) {
                removeNode();
            }
            node = new Node(key, val);
            nodeMap.put(key, node);
            // 新加入的默认移动到头结点
            moveHead(node);
            count++;
        } else {
            // 先移除原先节点,然后再移动到头结点
            removeNode(node);
            moveHead(node);
        }
    }

    public Node get(K key) {
        Node node = nodeMap.get(key);
        if (null != node) {
            // 先移除原先节点,然后再移动到头结点
            removeNode(node);
            moveHead(node);
        }
        return node;
    }

    /**
     * 测试用,按链表顺序输出
     */
    public void printNode(Node node) {
        if (null != node) {
            if (null != node.key)
                System.out.println(node.key +", value:" + node.value);
            printNode(node.next);
        }
    }

    public Node<K, V> getHead() {
        return head;
    }

    /**
     * 移除末尾节点
     */
    private void removeNode() {
        Node temp = tail.pre;
        removeNode(temp);
        nodeMap.remove(temp.key);
        count--;
    }

    /**
     * 移除指定节点,该方法只作删除,不作容量减少
     * @param node
     */
    private void removeNode(Node node) {
        Node next = node.next;
        Node pre = node.pre;
        next.pre = pre;
        pre.next = next;
        node.next = null;
        node.pre = null;
    }

    /**
     * 移动到头结点
     * @param node
     */
    private void moveHead(Node node) {
        Node temp = head.next;
        temp.pre = node;
        node.next = temp;
        node.pre = head;
        head.next = node;
    }

}

class Node<K, V> {
    K key;
    V value;
    /**
     * 上一节点引用
     */
    Node pre;
    /**
     * 下一节点引用
     */
    Node next;
    public Node (K key, V value) {
        this.key = key;
        this.value = value;
    }
}

11、redis的lru算法实现

redis采用的是近似lru算法实现,即并不是严格意义上的lru实现,近似lru通过随机采样法淘汰数据,每次取出5个key(默认是5个,可通过修改配置文件修改样本数,建议10个,其淘汰结果最接近严格意义上的算法),然后从这些key里面淘汰最近最少使用,redis为了实现近似lru算法,给每个key增加了一个26bit的字段,用于存储该key最后一次被访问的时间
redis3.0对近似lru算法进行了优化,新算法会维护一个候选池,池中的数据根据访问时间(访问时间越大,说明最近被访问)进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小访问时间才会被放入池中
redis4.0新加了lfu算法,核心思想是根据key的最近被访问的频率进行淘汰,根据频率来比较的话相对lru算法来说更准确,lfu也有两种策略:volatile-lfu、allkeys-lfu

12、redis中zset关于跳表的数据结构

跳跃表(skiplist)是一种基于有序链表的扩展(多层链表结构),即在原先链表上提取出关键节点作分层处理,即索引,查找时先从从索引层开始,然后再回到原链表,保证上一层节点数是下一层的一半,链表新增时同样是先确定插入的位置,然后放进去之后需要动态增加索引节点,这里采用的是抛硬币的方式,即50%的概率判断是否需要提拔到上一层作为索引节点。其查询/插入/删除 复杂度o(lgn),换句话来说,跳表是可以实现二分查找的有序链表

13、redis的管道pipeline

管道pipeline可以一次性发送多条命令并在执行完后一次性将结果返回,其通过减少客户端与redis的通信次数来实现降低往返延时时间,而且pipeline实现的原理是队列,遵循先进先出,因此保证了数据的顺序性。

14、transient关键字

transient关键字就是说让某些被修饰的成员属性变量不被序列化,当某些属性的生命周期仅存于调用者的内存中,而不想被写到磁盘里持久化或一些敏感属性不想在网络操作中被传输,此时可以使用transient关键字,其最大作用就是为了节省存储空间,但也会导致某些属性需要重新计算,总的来说利大于弊。
ps:一个静态变量不管是否被transient修饰,都不能被序列化

15、进程简介及进程间通信方式

进程可以认为是程序执行时的一个实例,是系统进行资源分配和调度的基本单位,每个进程都拥有独立的内存空间,一个进程无法直接访问另一个进程的变量和数据结构,进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,常用的通信方式大概有5种:管道、信号、消息队列、共享内存、套接字

16、为什么不推荐使用Executors直接创建线程池

1)LinkedBlockingQueue缓存队列没有指定最大值,导致其是无界的,无限增大最终将内存撑爆
2)部分线程池的线程数是Integer.MAX_VALUE,由于这个数量级已经够大,当执行线程数的增多和线程没有及时回收,最终也会将内存撑爆
建议使用ThreadPoolExecutor的方式自己新建线程池

17、如何控制线程池线程的优先级

jdk缺省的线程池不支持优先级调度的功能,需要使用PriorityBlockingQueue替代BlockingQueue,优先级队列要求放入队列的线程必须实现Comparable接口,该队列会根据内部存储的每个元素的compareTo方法比较每个元素的大小

18、Spring如何解决循环依赖

(详细请看笔记)简单版:spring使用三级缓存来解决field属性的循环依赖,主要是基于java的引用传递特性实现,当获取到对象的引用时,对象的属性是可以延后设置的,在初始化的第一步完成后就将该对象加入到三级缓存中,供外部使用

19、事务隔离级别

读未提交(Read uncommitted):能读到未提交的内容,无特殊情况一般不会该种隔离级别
读已提交(Read committed):只能读到已提交的内容,能避免出现脏读现象,Sql Server和Oracle的默认隔离级别即为它
可重复读(Repeatable read):为了避免不可重复读现象,mysql的默认隔离级别即为它
串行化(Serializable):事务串行化顺序执行,一个一个排队等待,效率低基本不用

20、mysql的MVCC多版本并发控制

由于MVCC并没有一个统一的实现标准,因此不同的数据库不同的存储引擎所实现的MVCC都不尽相同,以Innodb版本为例,MVCC是通过保存数据在某个时间点的快照来实现的,通过这个快照可以提供一定级别(语句级或事物级)的一致性读取,不管事务执行多长时间,事务看到的数据都是一致的。
MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突,缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。
Innodb通过undo log保存了已更改行的旧版本的信息的快照,其内部实现中为每一行数据增加了三个隐藏列用于实现MVCC
ps:MVCC只在READ COMMITED 和 REPEATABLE READ 两个隔离级别下工作

21、Innodb的行锁

Innodb实现了两种形式的行锁,分为共享锁和排他锁。
共享锁:又称读锁,多个事务对于同一行数据可共享一把锁,都能访问到数据,但是只能读不能改,可通过select...lock in share mode语句加锁
排他锁:又称写锁,该锁不能与其他锁并存,但是获取该锁的事务可以对数据进行读取和修改,可通过select...for update语句加锁
Innodb中还存在两类意向锁,均属于表锁,简单说来存在意向锁,则必定存在行级锁,其作用就是为了试图在某个表应用共享或排他锁时无需检查该表的所有行,而只需检查表上是否有意向锁即可。
ps:Innodb行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,Innodb才会使用行锁,否则将使用表锁;同时排他锁必须在事务块(begin/commit)中才能生效

22、MyIsam和Innodb的区别?何时使用MyIsam

前者不支持外键、事务、行锁,执行新增或更新操作时使用的是表级锁定,查询时该引擎效率很高,占用的存储空间小;后者均支持,占用的存储空间较大,也是mysql5.7版本之后默认的存储引擎。
如果你的应用对查询性能要求较高,就需要使用MyIsam,它的索引和数据是分开的(非聚集索引),而且其索引是压缩的,可以更好的利用内存,所以它的查询性能明显优于Innodb。

23、为什么说B+树比B树更适合数据库索引?

B树在解决了IO性能的同时并没有解决元素遍历的效率低下问题,因此B+树应运而生,B+树只需要去遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围查询是非常频繁的,而B树不支持这样的操作或者说效率太慢。
B+树:非叶节点仅保存索引,所有与记录相关的信息都存在叶子节点,且所有叶子节点组成一个有序链表。
文件系统和数据库的索引数据都存在磁盘上的,当数据量过大的时候可能无法一次性加载进内存,这时B/B+树的多路存储威力就出来了,可以每次加载进B/B+树的一个节点,然后一步步往下找;同时大规模数据存到磁盘的时候,定位数据也是一个非常耗费时间的操作,通过B/B+树进行优化后便能提高磁盘读取时定位的效率,这一切都取决于他们的多路存储。

24、为什么JDBC需要打破双亲委派模型

主要是因为原生的jdbc中Driver驱动本身只是一个接口,具体的实现是由不同数据库厂商去实现的,而原生的jdbc中的类是放在rt.jar包的,是由启动类加载器进行加载的,然后在jdbc中的driver类需要去动态加载不同数据库类型的diver类,而启动类加载器肯定无法加载这些自定义实现的类,那么则需要交由子加载器去加载这些类,那么如何在父加载器加载的类中,去调用子加载器去加载类呢?因此java引入了一个线程上下文加载器(该加载器可以在在创建线程时指定类加载器,如果没指定则先从父线程中继承一个,如果全局范围内都没有,则默认是应用程序类加载器)

25、Spring中BeanFactory和ApplicationContext的区别

BeanFactory:spring中比较古老和原始的factory,无法支持spring插件,如:aop、web应用等
ApplicationContext:是BeanFactory的子类,由于古老的BeanFactory无法满足不断更新的spring的需求,于是ApplicationContext就基本替代了它的工作,以一种更面向框架的工作方式以及对上下文进行分层和实现继承,并在这个基础上对功能进行扩展,前者可以完成的,后者基本都能完成。
区别:使用BeanFactory加载xml时,只是说实例化了该容器,而容器中的bean并不会被实例化,只有在用到的时候才会去初始化(显示调用getBean);而使用ApplicationContext加载xxx.xml时,会实例化所有singleton的bean,好处是可以预先加载,坏处便是浪费内存

26、CDN是什么

(详细请看笔记)CDN全称叫Content Delivery Network,即内容分发网络。简单说来,CDN的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网络时。利用全局负载均衡技术将用户的访问指向距离最近的工作正常的压力最小的缓存服务器上,由缓存服务器直接相应用户请求。

CDN是一个策略性部署的整体系统,包括分布式存储、负载均衡、网络请求的重定向和内容管理4个要件,而内容管理和全局的网络流量管理(Traffic Management)是CDN的核心所在。其特点便是通过用户就近性和服务器负载的判断,CDN确保内容以一种极为高效的方式为用户的请求提供服务。

27、哈希冲突是什么及解决方式

哈希冲突/碰撞:假设hash表的大小为9,现在要将一串数据存到表里:28,19,15,20,33,12,17,10,5,简单计算下hash(28)=1,然后将28这个值存到标记为1的槽里,接着hash(19)=1,此时发现1这个槽里已经存在数据28了,此时则为发生了哈希冲突或者说碰撞,一个好的哈希算法此时会重新计算这个值的存储空间尽量减少碰撞
解决方式:
1)开放地址法,也称再散列法,若哈希地址p冲突了,以p为基础产生另一个哈希地址p1,若p1还是冲突,再以p1为基础循环产生新的哈希地址,直到找到不重复的哈希地址,该方法有一个通用的再散列函数形式,再散列方法有三种:线性探测再散列、二次探测再散列、伪随机探测再散列
2)再哈希法,同时构造多个不同的哈希函数,当第一个计算冲突时,计算第二个函数,直到不再产生冲突,该方法不易产生聚集,但增加了计算时间
3)链地址法,也称拉链法,该方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行,链地址法适用于经常进行插入和删除的情况
4)建立公共溢出区,该方法的基本思想是将哈希表分为基本表和溢出表两部分,凡是跟基本表产生冲突的元素,一律填入溢出表

28、二分查找简介

是一种在有序数组中用于查询某一特定元素的搜索算法,每次搜索时先从数组的中间位置开始,若待搜寻元素正好等于中间元素则返回;若大于则从后半区间继续判断,每一次新的判断也都是从区间的中间元素开始。

29、happens-before原则

即先行发生原则,该原则是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以在并发环境下解决两操作之间是否可能存在冲突的所有问题。如果说A操作先行发生于B操作,其实就是说在发生B操作之前,A操作产生的影响能被B观察到,”影响“包括修改了内存中共享变量的值、发送了消息、调用了方法等。(当然两个操作之间存在happens-before关系,并不意味着一定要按照该原则制定的顺序来执行,如果重排序之后执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法)

happens-before八大原则如下:
1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
2)管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
5)线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
7)线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

30、java内存模型简介

jvm将内存组织分为主内存和工作内存,主内存主要包括本地方法区和堆,每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主内存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)
1)java中所有共享变量都存储在主内存
2)线程对共享变量的操作都必须在自己的工作内存中进行,不能直接在主内存中读写
3)不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存进行(假设线程1修改了某个共享变量的值,线程2想要看到的话,必须线程1需要先将该值更新到主内存,然后主内存将该值更新到线程2的工作内存中)

31、如何保证可见性

多线程运行时可能会出现可见性问题,但是我们需要明确在单核cpu情况下是不存在可见性问题的,只有多核cpu才会出现该问题,当对一个普通变量进行读写时,线程会先从内存中拷贝该变量到cpu缓存中(因为cpu是不会直接操作内存的,操作缓存比操作内存更快),然后cpu对缓存中的数据进行修改,但是在多核cpu情况下,每个线程会拷贝到不同的cpu缓存中,此时便会出现可见性问题(因为不同的缓存之间是无法感知到对这个变量的修改)
volatile:主要通过两个指令来保证(load和store),每次对volatile变量进行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;在读取操作时,会加入一条load指令,即强迫从主内存中读入变量的值。但volatile不保证原子性,因为会存在让出cpu执行权的情况,
synchronized:使用synchronized关键字后,线程在加锁时,会先清空工作内存,在主内存中拷贝最新变量的副本到工作内存,执行完代码后将修改后的共享变量的值回写到主内存中,最后释放互斥锁,以此保证可见性。

32、UML中四大关系

UML即 Unified Modeling Language,统一建模语言,UML的类图中四大关系为泛化、实现、关联、依赖。泛化对应于继承,存在于类和类,实现对应于implements实现,存在于类和接口。关联依赖则比较像,关联使的某个类能知道另一个类的属性和方法,是具有持久性的,比如类B作为类A的参数变量或类A引用了类B的实例变量。而依赖则属于临时性、偶发性的,关系比较弱,表示类与类之间的连接,表示一个类依赖于另外一个类的定义,比如类B作为类A的方法参数,A方法的参数是B的属性或A方法的返回值是B,举个例子(我吃饭时使用筷子,我与筷子就属于依赖,绝大部分场景下我不需要筷子,但是在吃饭时我离不开它)

33、tcp长连接和短连接的区别

长连接是指在一个tcp连接内,可产生多次数据传输,当无数据传输时,需要双方发送检测包以维持此连接(心跳机制),总的过程如下:
连接 = > 数据传输 => 。。心跳检测 =>数据传输 =》关闭连接
短连接则是在需要进行数据传输时才建立一个tcp连接,数据传输完成后就断开此连接,每一次建立tcp连接都需要三次握手和四次挥手:
连接 = > 数据传输 => 关闭连接

34、父类的静态方法能否被子类重写

不能,静态方法只与类有关,不与实例有关,重写只适用于实例方法,而非静态方法。静态方法是程序一运行就已经分配好了内存地址,而且该地址是固定的,所有引用到该方法的对象所指向的始终是同一个内存地址中的数据,如果子类定义了相同名称的静态方法,只会新增一个内存地址,并不会重写。

35、抽象类的意义

1)为子类提供一个公共的类型

2)封装子类中重复的内容(成员变量和方法)

3)定义有抽象方法,子类虽然有不同的实现,但该方法的定义是一致的

36、静态内部类的意义

首先我们在项目中也经常使用内部类,通常使用内部类的最大优点便是其非常好的解决了多重继承的问题(每个内部类均能继承一个实现,所以无论外部类是否已经继承,对于内部类都没有影响)。

那对于静态内部类,其职能访问外部类的静态属性和方法,这是与一般内部类的区别,而且静态内部类还可以有静态数据,静态方法或者又一个静态内部类,这些是非静态内部类所没有的。非静态的内部类既可以访问外部类的静态成员方法与成员变量, 也可以访问外部类的非静态成员方法与成员变量。而静态内部类不能访问外部中的非静态,因此静态内部类的主要意义可能在工具类这一方面。

37、谈谈解析与分派

这两者都处于方法调用的过程中(方法调用并不等同于方法执行),所谓方法调用是指程序需要确定方法的版本即调用哪一个方法,首先我们需要知道一切方法调用在class文件里面存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用),因此java方法调用需要在类加载期间甚至是运行期间才能确定目标方法的直接引用。

解析:在类加载的解析阶段,会将其中的一部分符号引用转为直接引用,当然这个前提是:这些方法在程序真正运行之前就已经确定了,并且在运行期间不可改变,比如静态方法、私有方法就属于这一类型(编译器可知,运行期不可变)

分派:分派调用过程也是java多态性的一个具体体现,即重载、重写都是通过分派实现的。分派又可分为静态分派(重载)和动态分派(重写)

静态分派:依赖静态类型来确定方法执行版本的分配过程称为静态分派,静态分派的典型应用是方法重载,其发生于编译阶段,下面举个例子来理解下什么是静态类型和实际类型。

针对Human man = new Man();

我们把Human称为变量的静态类型,后面的Man则称为实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型并不会改变,并且最终的静态类型是在编译器可知的;而实际类型变化的结果只有在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。变化可见下图:

那么虚拟机(准确来说是编译器)在重载时选择哪个版本调用是通过参数的静态类型而不是实际类型作为判定依据的。因此上面的代码中输出的都是hello guy

动态分派:在运行期根据实际类型来确定方法执行版本的分派过程称为动态分派,典型应用便是方法重写。java虚拟机是通过”稳定优化“的手段--在方法区中建立一个虚方法表,通过使用方法表的索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那么子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口;如果在子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。

38、String为什么要设计成不可变的

主要是三方面因素:设计考虑、性能、安全
1)由于字符串常量池的存在(在jdk1.8中,字符串常量池已经从原先方法区中的运行时常量池分离到堆中了),当创建一个String对象时,如果该字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;假如字符串对象允许改变,将会造成各种逻辑错误,比如改变一个对象会影响到另一个对象。
2)字符串不变性保证了哈希码的唯一性,因此可以放心的进行缓存,不必每次都去计算新的哈希码
3)String被许多的Java类/库用来当做参数,例如:网络连接地址、文件路径等,假如字符串对象不是固定不变的,将会引起各种安全隐患

39、简单说一下泛型

泛型可以认为是java语言的一颗语法糖,其具体实现方法称为类型擦除,java语言的泛型只在程序的源代码中存在,在编译后的class文件中就已替换为原来的原生类型,可以认为是裸类型,并且在响应的地方插入了强制类型转换代码,因此对于运行的java来说,List<Integer>和List<String>是同一个类(List),基于这种实现的泛型称为伪泛型

40、谈谈浅拷贝和深拷贝

浅拷贝:指在拷贝对象时,对于基本数据类型的变量会重新复制一份,而对于引用类型的变量则只会对引用进行拷贝,举个例子,A类中存在引用变量B,现对A进行拷贝成A1,若此时对A类中的变量B进行修改,A1的B值也会随着修改,换句话说,这两个对象的地址是一样的
深拷贝:则是对对象及该对象关联的对象内容,都会进行一份拷贝,但是需要每个被引用的类都实现cloneable接口,当类之间层级嵌套比较深的时候会非常麻烦。因此实际写代码时常有另外两种方式:流方式和json转换。这两种方式由于都是先将对象转换成字节流或json,然后再次读取作为一个新对象返回,那么这个新对象自然和源对象不存在任何地址上的共享。

41、tcp可靠性传输的实现

1)确认应答机制(ACK):TCP对每个字节的数据都进行编号,即为序列号,确认号=序列号+1,每个ACK都有对应的确认号,意思是告诉发送者已经接收到了数据,下一个数据应该从哪里发送。
2)超时重传机制:当一个主机长时间没有收到对方发来的报文,我们可以认为是ACK丢了,这时就需要触发超时重传机制
3)连接管理机制:由于TCP是面向连接的,因此可进行可靠性传输
那么udp如何实现可靠性传输?
参考tcp,引入序列号,保证数据顺序;引入确认应答,保证对端收到了数据;引入超时重传,如果隔一段时间没有应答,就重新发送数据。

更多tcp知识走:TCP三次握手和四次挥手

42、聚集索引和非聚集索引

Innodb是聚集索引,普通索引也叫二级索引,除聚集索引外的索引即非聚集索引,MyIsam只是非聚集索引,MyIsam的非聚集索引的索引和数据文件是分离的,索引保存的是数据文件的指针(注意区分Innodb的非聚集索引与MyIsam的非聚集索引)。聚集索引一般是表中的主键索引,如果表中没有指定主键,Innodb则会选择表中的第一个不为空的唯一索引作为聚集索引,如果还是没有,则生成一个隐式的聚集索引。两者的根本区别是表记录的排列顺序和与索引的排列顺序是否一致。
1)由于聚集索引规定了数据在表中的物理存储顺序,因此一个表只能有一个,但该索引可以包含多个列(组合索引)而非聚集索引一个表可以存在多个。
2)聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续。
3)聚集索引查询数据速度快,插入数据速度慢;非聚集索引反之。
聚集索引和非聚集索引都采用了B+树的结构,聚集索引的叶子节点存储的是行记录(其实是页结构,一个页包含多行数据),因此使用聚集索引查询会很快,因为会直接定位到行记录;InnoDB的非聚集索引存储的是主键(聚集索引)的值,而MyIsam的普通索引存储的是记录指针,指向对应的数据块

43、分布式与集群的差别

简单点说,分布式是并联工作,而集群是串联工作。分布式是将多个业务分发到不同的服务器上来共同完成工作,而集群是将多台服务器集中在一起,来实现同一业务;分布式中的每一个节点都可以做集群,而集群并不一定是分布式的;分布式中的每一个节点完成不同的业务,如果某个节点挂了,那么该项业务就无法访问,而集群有一定的组织性,一台服务器挂了,另一台服务器可以顶上来。
官方一点说,分布式是以缩短单个任务的执行时间来提升效率的;而集群是提高单位时间内执行的任务数来提升效率。

举个小例子:
小饭店原来只有一个厨师,切菜洗菜备料炒菜全干。后来客人多了,厨房一个厨师忙不过来,又请了个厨师,两个厨师都能炒一样的菜,这两个厨师的关系是集群。为了让厨师专心炒菜,把菜做到极致,又请了个配菜师负责切菜,备菜,备料,厨师和配菜师的关系是分布式,一个配菜师也忙不过来了,又请了个配菜师,两个配菜师关系是集群

44、为什么不推荐直接使用Log4j、Logback等日志框架的API?

主要是为了解耦,每一种日志框架都有自己单独的API,要使用某一种框架则必须要使用它的API,这就大大的增加应用程序代码对于日志框架的耦合性,因此建议通过Slf4j日志门面来使用,Slf4j采用外观模式实现,是一套包装Logging框架的界面程式,通过外观模式的设计,我们不需要了解Slf4j下具体日志框架的实现,只需要调用它暴露出来的API来输出日志,当程序需要更换一个日志框架时,也仅仅需要替换下jar包,修改下配置文件,完成解耦功能。

45、java如何打印方法的调用堆栈?

主要是为了查看该方法被哪些上层所调用,可以通过如下方式来打印出调用者的类名、方法名、方法行数等

        StackTraceElement[] elements = Thread.currentThread().getStackTrace();
        for (StackTraceElement element : elements) {
            logger.info("---------");
            logger.info("className:{}, methodName:{}, fileName:{}, lineNumber:{}", element.getClassName(), element.getMethodName(), element.getFileName(), element.getLineNumber());
        }

46、如何进行mysql索引优化?

1)首先我们可以在开发环境中开启mysql的慢查询,以便能找出有问题的sql语句,当慢查询日志过多的时候,可以使用mysql提供的mysqldumpslow工具进行分析,使用它可以找出输出次数最多的几条sql语句。

2)拿到具体的sql语句后便可以使用explain进行调优,重点关注type和extra两列,判读索引是否生效(失效情况:like关键字匹配时第一个字符为%,索引列上进行操作如计算、函数等,),尤其是组合索引要看是否满足最左前缀匹配,在优化时要结合业务方判读是否需要全部的查询列,而且还需要分析其他相关的查询,以确定最终的组合索引顺序使之满足最左前缀匹配,同时查询语句中尽量使用覆盖索引,尽量减少回表查询。

3)索引不是越多越好,要尽可能的扩展索引而不是新建索引,扩展索引即建立组合索引

47、什么是内存泄漏

对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

引起内存泄漏的根本原因就是长生命周期对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是java中发生内存泄漏的场景。比较容易发生内存泄漏的场景有:静态集合、监听器、各种连接等等。

48、为什么静态方法不能直接调用非静态成员(方法和变量)

因为静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问,非静态成员属于类的对象,所以在只有在对象实例化后才会存在,然后也只能通过类的对象进行访问。所以说在一个类的静态方法中去访问非静态成员主要就是因为此时非静态成员还不一定存在(因为实例对象不一定存在),静态方法先与非静态成员存在,所以编译器会直接报错,避免之后的运行时异常。

49、java使用什么数据类型来表示金额

首先不能使用float、double等浮点数,因为浮点数参与的运算通常伴随着因为无法精确表示而进行近似或舍入,所以导致结果会丝毫的偏差,而涉及金额的计算是绝对不允许存在偏差的。
java提供了一种数据类型可以精确的标识浮点数,即BigDecimal,它提供了多个重载的构造方法,只有用参数为String类型的构造方法才能精确表示,小例子如下:

    @Test
    public void test() {
        double a = 0.3;
        double b = 0.2;
        System.out.println("double 运行结果:" + (a - b));

        float c = 0.3f;
        float d = 0.2f;
        System.out.println("float 运行结果:" + (c - d));

        BigDecimal bd1 = new BigDecimal(0.3);
        BigDecimal bd2 = new BigDecimal(0.2);
        System.out.println("BigDecimal 参数为double运行结果:" + (bd1.subtract(bd2)));

        BigDecimal bd3 = new BigDecimal("0.3");
        BigDecimal bd4 = new BigDecimal("0.2");
        System.out.println("BigDecimal 参数为String运行结果:" + (bd3.subtract(bd4)));
    }

输出结果如下:

double 运行结果:0.09999999999999998
float 运行结果:0.10000001
BigDecimal 参数为double运行结果:0.099999999999999977795539507496869191527366638183593750
BigDecimal 参数为String运行结果:0.1

猜你喜欢

转载自blog.csdn.net/m0_38001814/article/details/105690682