秋招面试题(字节跳动)

目录

 

1.GC怎么回收,分代回收

2.垃圾回收算法

3.常见的垃圾回收器

4.ThreadLocal的内部实现,

(1)介绍

(2)原理

(3)内存泄露问题

5.Spring的特点、IOC和AOP,bean生命周期

(1)Spring

(2)IOC

(3)AOP面向切面编程

(4)Bean的生命周期

6.java类加载过程

7.双亲模式

8.为什么需要双亲委派模型?

9.打破双亲委派模式

10.LRU及优化(Redis就是采用这个)

11.海量数据如何找前1000个?如何优化?

12 redis分布式锁(很重要)

(1)介绍

(2)实现

(3)流程

13 场景题

14、如何写一个死锁程序

15 https过程

14、cyclicbarrier和countdownlatch的区别

15、信号量

16、Servlet生命周期

17、数据库分库分表

(1)介绍

(2)种类

18 注解

19.平衡二叉树查找复杂度

20.HTTP数据量很大,怎么发送

21.大文本数据(数T),统计每个字符串的频率

22.TCP server最多可以建立多少个TCP连接

23.什么是跨域?如何解决跨域问题?

24.java 深浅拷贝

25.输入url的整个过程

26.状态码

27.数据库sql中 in和exists区别

28.泛型,和泛型的擦除

29.http请求报文结构

30.redis实现页面缓存

31.开发中日志:Springboot与日志

32.基本数据类型(比如int i)在内存中是怎么存的

33.类对象什么时候加载的

34.static的原理

35.写日志类满足多线程向文件中写日志,设计一下需要实现哪些方法

36.深克隆和浅克隆

37.java多态的原理


1.GC怎么回收,分代回收

可达性分析后不可达的对象不是马上进行回收的,需要进行两次标记过程。第一次标记:判断该对象是否不要执行finalize()方法,如果该对象已经执行过finalize()方法或者没有重写该方法,则代表不需要执行,这样直接将该对象进行第二次标记,被回收;如果该对象需要执行finalize()方法,则将该对象放入到F-queue队列中,jvm自动创建一个低优先级的Finalizer线程执行对象的finalize()方法。再执行finalize()方法时,对象如果能够重新建立连接,那么该对象将摆脱这次回收,如果该对象没有重新建立连接,那么对该对象进行二次标记。二次标记的对象进行回收。

2.垃圾回收算法

(1)标记清除

(2)标记整理

(3)复制算法(年轻代)

(4)分代收集算法

3.常见的垃圾回收器

(1)Serial收集器:(串行收集器)最早的垃圾回收器。GC时,其他线程不能执行,用户体验差。

(2)ParNew收集器:Serial收集器的多线程版本(使用多条线程进行GC)允许垃圾回收时,其他线程并行执行

(3)ParNew Scanvenge收集器:具有高的吞吐量和自适应的调节策略来支持高吞吐。

(4)CMS收集器:(老年代收集器)一种以获取最短的停顿时间为目的的回收器。

缺点:

  • 对cpu资源非常敏感,占用cpu资源会导致程序运行慢;
  • 无法回收浮动垃圾:边收集边产生,只能下一次再回收;
  • 会产生大量碎片:标记清除,一定会产生大量碎片的;

(5)G1收集器:是一款面向服务端应用的垃圾回收器。

G1它将整个Java堆划分为多个大小相等的独立区域,虽然保留了新生代和老年代,但是和之前不同的是,新生代和老年代不再隔离开,他们是独立区域(不连续)的集合。这样在收集的时候就不是对全区域回收,而是根据各个区域里面堆积垃圾值大小,在后台维护一个优先级列表,根据允许回收的时间,回收优先级高的区域。这样可以在优先的时间内提高效率缩短停顿时间。

优点:

  • 不会产生空间碎片:整体采用的是标记整理,局部采用的是复制算法,都有不会产生碎片
  • 可以预测的停顿:可以让使用者明确指定停顿时间。(可以指定一个最小时间,超过这个时间,就不会进行回收了)它有了这么高效率的原因之一就是:对垃圾回收进行了划分优先级的操作,这种有优先级的区域回收方式保证了它的高效率。
  • 缩短停顿时间:采用多cpu

4.ThreadLocal的内部实现,

ThreadLocal并不是用来并发控制访问一个共同对象,而是为了给每个线程分配一个只属于该线程的对象(这么粗暴的解释可能还不太准确),更准确的说是为了实现线程间的数据隔离。而ThreadLocal应用场景更多是想共享一个变量,但是该变量又不是线程安全的,那么可以用ThreadLocal维护一个线程一个实例。有时候ThreadLocal也可以用来避免一些参数传递,通过ThreadLocal来访问对象。

(1)介绍

首先介绍Thread类中属性threadLocals:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
    我们发现Thread并没有提供成员变量threadLocals的设置与访问的方法,那么每个线程的实例threadLocals参数我们如何操作呢?这时我们的主角:ThreadLocal就登场了。
  所以有那么一句总结:ThreadLocal是线程Thread中属性threadLocals的管理者
也就是说我们对于ThreadLocal的get, set,remove的操作结果都是针对当前线程Thread实例的threadLocals存,取,删除操作。类似于一个开发者的任务,产品经理左右不了,产品经理只能通过技术leader来给开发者分配任务。下面再举个栗子,进一步说明他们之间的关系:

(2)原理

1、内部机构和方法解析:ThreadLocal包含三个方法get, set , remove以及内部类`ThreadLocalMap

2、Thread和threadLocals的关系

线程共享变量缓存如下:

Thread.ThreadLocalMap<ThreadLocal, Object>;

(a) Thread: 当前线程,可以通过Thread.currentThread()获取。

(b) ThreadLocal:我们的static ThreadLocal变量。

(c) Object: 当前线程共享变量。

我们调用ThreadLocal.get方法时,实际上是从当前线程中获取ThreadLocalMap<ThreadLocal, Object>,然后根据当前ThreadLocal获取当前线程共享变量Object

ThreadLocal.set,ThreadLocal.remove实际上是同样的道理

这种存储结构的好处:

1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。

2、ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。

(3)内存泄露问题

关于ThreadLocalMap<ThreadLocal, Object>弱引用问题:

当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。

虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,我们有两种手段。

1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;

2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

5.Spring的特点、IOC和AOP,bean生命周期

(1)Spring

1、非侵入式编程

   Spring框架的API不会再业务逻辑上出现,即业务逻辑是POJO(Plain Ordinary Java Object)。由于业务逻辑中没有Spring的API,所以业务逻辑可以从Spring框架快速的移植到其他框架。

2、容器

  Spring作为一个容器,可以管理对象的生命周期、对象与对象之间的依赖关系。可以通过配置文件来定义对象,以及设置其他对象的依赖关系。

3、IoC

   控制反转(Inversion of Control),即创建被调用的实例不是由调用者完成,而是由Spring容器完成,并注入调用者。

  当应用IoC,一个对象依赖的其他对象会通过被动的方式传递进来,而不是这个对象自己创建或查找依赖对象,即,不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。

3、AOP

    面向切面编程,是一种编程思想,是面向对象编程OOP的补充。Spring提供面向对象编程的支持,允许通过分离应用的业务逻辑与系统级服务(日志和事务管理)进行开发。应用对象只实现他们应该做的(完成业务逻辑),并不负责其它的系统级关注点(日志或者事务的支持)。

  可以把日志、安全、事务管理等服务理解成一个“切面”,把很多被业务逻辑反复使用的服务完全剥离出来,以达到复用。然后将“切面”动态的“织入”到业务逻辑中,让其享受此“切面”的服务。

(2)IOC

IOC(Inversion of Control)控制反转,DI(Dependency Injection)依赖注入,其实两者本质上是没有区别的,就是参考的角度不同:

对于Spring容器:它创建为需要Bean的程序创建Bean,并注入给程序,这就是依赖注入;

控制反转:原来程序是自己创建需要的Bean,现在是Spring创建好了,主动变被动,就是控制反转。

例如:在A类中调用B类的方法,那么我们就称 A依赖B,B为被依赖(对象),相信这点大家能够理解。

在spring中,B的实例对象被看成Bean对象,这个Bean对象由spring容器进行创建和管理,如此一来,A获取B的实例对象就不是由自己主动去获取,而是被动接受spring给它设值,那么,这个主动变为被动,就可以理解为“控制反转”。

而另一种说法,从spring容器的角度上看,它负责把A的依赖对象B(B是被依赖对象)注入给了A,所以我们可以理解为“依赖注入”

(3)AOP面向切面编程

https://www.cnblogs.com/hq233/p/6637488.html

面向切面编程是对面向对象编程的有益补充 ,简单的说就是添加功能。

举个栗子:

有一个人类的接口,里面有吃饭这个抽象类,选择想要创建一个中国人这个类来实现这个接口和吃饭这个方法。再创建一个美国人,韩国人......这是面向对象编程。但是现在我们要求实现一个功能,就是中国人和美国人等5个国家的类吃饭前要有洗手这个方法吃饭后要有漱口这个方法,其他的国家可能不需要。我们可以把这五个类都添加这两个方法,但是会出现代码复用,所以提出了面向切面编程。就是吃饭这个动作执行前,执行洗手方法,执行后,执行漱口方法。

(4)Bean的生命周期

1.Spring对Bean进行实例化,相当于new的过程。
2.完成DI即依赖注入,将依赖的值和引用注入到Bean对应的属性中。
3.判断当前实例化的Bean是否implements接口BeanNameAware,Spring将调用接口方法setBeanName(),传入Bean的ID;
4.判断当前实例化的Bean是否implements接口BeanFactoryAware,Spring将调用接口方法setBeanFactory(),传入BeanFactory容器实例;
5.判断当前实例化的Bean是否implements接口ApplicationContextAware,Spring将调用接口方法setApplicationContext(),传入Bean所在的ApplicationContext实例;
6.判断当前实例化的Bean是否implements接口BeanPostProcessor,Spring将调用接口方法postProcessBeforeInitialization();
7.判断当前实例化的Bean是否implements接口InitializingBean,Spring将调用接口方法afterPropertiesSet()。如果Bean的init-method属性设置了初始化方法,则该方法也会被调用;
8.判断当前实例化的Bean是否implements接口BeanPostProcessor,Spring将调用接口方法postProcessAfterInitialization();
9.此时,Bean已准备就绪,可以被应用程序使用,并驻留在Spring应用上下文中,直到销毁;
10.当销毁Bean时,判断该Bean是否implements接口DisposableBean,Spring将调用接口方法destroy()。如果Bean的destroy-method属性设置了销毁方法,则该方法也会被调用。

Bean的完整生命周期从 spring 容器开始实例化 bean 开始,到销毁。可以从三点来理解

1、 bean自身的方法:包括构造方法、 set 方法、 init-method 指定的方法、 destroy-method 指定的方法

2、 Bean级生命周期接口方法:如 BeanNameAware 、 BeanFactoryAware 等这些接口方法由 bean类实现。

3、 容器级生命周期接口方法:上图中带星的。有InstantiationAwareBeanPostProcessor 、 BeanPostProcessor 等。一般称为后处理 器。他们一般不由bean 本身实现,独立存在,注册到 spring 容器中。 Spring 通过接口反射预先知道,当 spring 容器创建任何 bean 时,这些后处理器都会发生作用。所以他们是全局的,用户可以通过编码对只感兴趣的 bean 进行处理。

6.java类加载过程

在编译期:

将Java源文件也就是敲好的代码通过编译,编译成.class文件,也就是字节码文件(byte);

在运行期间:

然后将class文件通过加载器,经过Java虚拟机,最终转成操作系统需要的机器码文件。

所以,类加载过程就是将class加载到虚拟机的过程,包括以下7个过程(主要5个:加载,连接,初始化,使用,卸载)

(1)加载:通过class路径读取到相应的二进制,解析二进制流中的元数据(类型、常量)等,载入方法区,并在java堆中生成对应的class对象;

(2)验证:验证class文件是否合法;

(3)准备:就是分配内存,给类变量常量设置初始化的值(0,null...);

(4)解析:将符号引用替换为直接引用;

(5)初始化:静态变量赋值,静态代码块执行,还有构造方法;

7.双亲模式

自下向上检查是否加载,自上向下尝试加载:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

  • 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。负责加载系统类。
  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。加载扩展类。
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。加载应用类。

8.为什么需要双亲委派模型?

为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false

9.打破双亲委派模式

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法

默认的loadClass方法是实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时才由自己加载。

这里为了破坏双亲委派机制必须重写loadClass方法,即这里先尝试交由System类加载器加载,加载失败才会由自己加载。它并没有优先交给父类加载器,这就打破了双亲委派机制。

如果不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。而如果想打破双亲委派模型则需要重写loadClass()方法(当然其中的坑也不会少)。

10.LRU及优化(Redis就是采用这个)

https://blog.csdn.net/elricboa/article/details/78847305

原则:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

实现LRU
     
1.用一个数组来存储数据,给每一个数据项标记一个访问时间戳,每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。当数组空间已满时,将时间戳最大的数据项淘汰。

2.利用一个链表来实现,每次新插入数据的时候将新数据插到链表的头部;每次缓存命中(即数据被访问),则将数据移到链表头部;那么当链表满的时候,就将链表尾部的数据丢弃。

3.利用链表和hashmap(LinkedHashMap)。当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。

对于第一种方法,需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。对于第二种方法,链表在定位数据的时候时间复杂度为O(n)。所以在一般使用第三种方式来是实现LRU算法

11.海量数据如何找前1000个?如何优化?

https://zhuanlan.zhihu.com/p/36874277

最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。其实即使内存能够满足要求(我机器内存都是8GB),该方法也并不高效,因为题目的目的是寻找出最大的10000个数即可,而排序却是将所有的元素都排序了,做了很多的无用功。

第二种方法为局部淘汰法,该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。

第三种方法是分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较。

第四种方法是Hash法。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。

第五种方法采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

12 redis分布式锁(很重要)

https://github.com/redisson/redisson/wiki/目录

在多线程的时候,多个线程对一个key进行修改操作会出现线程不安全问题,因此需要对要操作的key加锁

(1)介绍

分布式锁就是在分布式环境下,保证同一时刻只有一个线程可以修改共享变量(数据库)。jdk提供的是能保证线程间的安全性,没有办法保证分布式各个节点间安全性,因此提出了解决分布式安全性的分布式锁

1、基于数据库实现分布式锁

2、基于缓存(redis)实现分布式锁

3、基于Zookeeper实现分布式锁;

一般对数据进行加锁时,要先获取锁,然后执行,然后释放锁。但Redis使用WATCH命令来代替对数据进行加锁,因为WATCH只会在数据被其他客户端抢先修改了的情况下通知执行了这个命令的客户端,而不是阻止其他客户端修改数据,所以WATCH被称为乐观锁

(2)实现

而具体的实现,则是基于两个redis的命令-SETNX和GETSET。
SETNX:SET if Not eXists,格式为SETNX key value,仅当key不存在时才会设置成功,返回1,否则返回0。这是加锁的基础,假设key名为lock.foo,只要有一个线程设置成功,那其他线程都无法再设置。
GETSET:GETSET key value,返回旧值,并将新的值设置进去。这个的作用后面会讲到。

(3)流程

(4)如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的

(5)java实现

 

13 场景题

https://www.eefung.com/company-news/5825a6ee-8576-416c-81e5-d35be3a3909e

1、海量日志数据,提取出某日访问百度次数最多的那个IP。分而治之+hash

2、请你统计最热门的10个查询串,要求使用的内存不能超过1G。(topk问题)

第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计;

第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘logK。最小堆。

14、如何写一个死锁程序

https://blog.csdn.net/qq_40261771/article/details/87904044

设计思想:

  • 1.设置两个线程A,B,设置两个资源标记对象a,b,
  • 2.用A去锁住a,用B去锁住b,使a,b都被占有,
  • 3.这时再用A去锁b,表示A目前需要b资源,因b目前被B占有,此时A无法锁住b
  • 4.同理,用B去锁a,B也无法去锁住a,形成死锁,A,B陷于无限等待,形成死锁

双重的synchronized 锁

15 https过程

http://www.sohu.com/a/320475422_120168610

HTTPS就是使用SSL/TLS协议进行加密传输,让客户端拿到服务器的公钥,然后客户端随机生成一个对称加密的秘钥使用公钥加密,传输给服务端,后续的所有信息都通过该对称秘钥进行加密解密,完成整个HTTPS的流程

SSL协议通信过程

(1) 浏览器发送一个连接请求给服务器;服务器将自己的证书(包含服务器公钥S_PuKey)、对称加密算法种类及其他相关信息返回客户端;

(2) 客户端浏览器检查服务器传送到CA证书是否由自己信赖的CA中心签发。若是,执行4步;否则,给客户一个警告信息:询问是否继续访问。

(3) 客户端浏览器比较证书里的信息,如证书有效期、服务器域名和公钥S_PK,与服务器传回的信息是否一致,如果一致,则浏览器完成对服务器的身份认证。

(4) 服务器要求客户端发送客户端证书(包含客户端公钥C_PuKey)、支持的对称加密方案及其他相关信息。收到后,服务器进行相同的身份认证,若没有通过验证,则拒绝连接;

(5) 服务器根据客户端浏览器发送到密码种类,选择一种加密程度最高的方案,用客户端公钥C_PuKey加密后通知到浏览器;

(6) 客户端通过私钥C_PrKey解密后,得知服务器选择的加密方案,并选择一个通话密钥key,接着用服务器公钥S_PuKey加密后发送给服务器;

(7) 服务器接收到的浏览器传送到消息,用私钥S_PrKey解密,获得通话密钥key。

(8) 接下来的数据传输都使用该对称密钥key进行加密。

14、cyclicbarrier和countdownlatch的区别

15、信号量

Semaphore 用来多线程互斥问题,相对于synchronized和Lock来说它允许多个线程访问一个临界区!例如各种池:数据库连接池、对象池等,这些池的需求就是同一时刻允许多个线程同时使用连接池。

Semaphore的模型可以概括为一个计数器、一个等待队列、三个方法。三个方法原子性分别是init()、down()、up();
init():设置计数器的初始值。
down():将计数器的值减一,如果减了一之后,计数器的值小于0,则当前的线程被阻塞,否则继续执行。
up():将计数器的值加一,如果加了一之后,计数器的值小于等于0,则唤醒等待队列中的一个线程,并且将它移除出等待队列。

简单的理解就是Semaphore就是通过这三个方法来改变计数器,通过计数器的值来判断此时的线程是应该加入到等待队列中等待还是成功执行

16、Servlet生命周期

1:加载Servlet

web容器负责加载Servlet,当web容器启动时或者是在第一次使用这个Servlet时,容器会负责创建Servlet实例,但是用户必须通过部署描述符(web.xml)指定Servlet的位置,也就是Servlet所在的类名称,成功加载后,web容器会通过反射的方式对Servlet进行实例化。

2:初始化

当一个Servlet初始化后,容器将调用init()方法初始化这个对象,初始化的目的是为了让Servlet在处理客户端请求前完成一些初始化的工作,如建立数据库连接,读取资源文件信息等,如果初始化失败,则次Servlet将被直接卸载。

3:进入服务

当有请求提交时,Servlet将调用service()方法进行处理,常用的是service根据请求类型调用doGet()或者doPost()方法进行处理;在service()方法中,Servlet可以通过ServletRequest接受客户的请求,也可以利用ServletResponse设置响应信息。

4:销毁

当web容器关闭或者检测到一个Servlet要从容器中被删除时,会自动调用destroy()方法,以便让该实例释放掉所占用的资源。

5:卸载

当一个Servlet调用完destroy()方法后,次实例将等待被垃圾收集器所回收,如果需要再次使用此Servlet时,会重新调用init()方法初始化。

Servlet类本质上也是一个普通的类,并且Servlet容器默认只允许单个实例存在。当请求达到服务器的时候,Servlet实例如果已经存在的话则直接加载该实例,如果该Servlet类还未实例化则会先初始化这个Servlet。当请求到达Web服务器时,Web服务器中有一个线程池,它会从线程池中取一个工作线程,通过该线程调用请求的Servlet。因此,对Servlet来说,可以同时被好几个请求调用。请求结束后,线程放回线程池。

这种设计带来的好处是,Servlet单实例,减少了生成Servlet的开销。通过线程池响应请求,避免了不断创建线程和销毁线程的开销,提高了性能。但是这种多线程操纵单实例的模式,也会有一些副作用,那就是可能造成数据的不一致。

解决办法:

1、去除实例变量,使用局部变量。(主要)

3、使用同步代码块:synchronized{…}

17、数据库分库分表

https://www.cnblogs.com/butterfly100/p/9034281.html

(1)介绍

关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。

(2)种类

数据切分根据其切分类型,可以分为两种方式:垂直(纵向)切分和水平(横向)切分

垂直分:

1、垂直分库

垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。如图:

垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。

垂直切分的优点

  • 解决业务系统层面的耦合,业务清晰
  • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

缺点

  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
  • 分布式事务处理复杂
  • 依然存在单表数据量过大的问题(需要水平切分)

2、水平切分

当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。

水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。如图所示: 

库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。

18 注解

注解属于Java语言的特性,是在Java5.0引入的新特征 ,位于java.lang.annotation包中 。 
注解概念: 
注解是用于给Java代码附加元数据,可在编译时或运行时解析并处理这些元数据。Java代码可以是包名、类、方法、成员变量、参数等,且附加的元数据不会影响源代码的执行。 
我们也可以这样通俗的理解Java注解:想像Java代码如包名、类、方法、成员变量、参数等都是具有生命,注解就是给代码中某些元素贴上去的一张标签。通俗点来讲,注解如同一张标签。这样理解有助于你快速地理解

19.平衡二叉树查找复杂度

在基于二叉排序树的查找里,我们可以得到的时间复杂度是在O(log2(n)到O(n))之间,如果不看建立平衡排序二叉树的过程,单纯看基于平衡二叉排序树的查找过程,我们会发现,卧槽,那种退化为顺序查找的可能性完全消失了,基于平衡排序二叉树的时间复杂度为O(log2(n))

20.HTTP数据量很大,怎么发送

首先要使用post方法进行请求,然后也可以采用分页的方式进行请求。

21.大文本数据(数T),统计每个字符串的频率

想到的就是MapReduce 的worldCount

22.TCP server最多可以建立多少个TCP连接

65535个,因为每个连接都有唯一的标识标定,多个连接通过多个accept就可以处理多个连接间的数据传输,所以服务器可以同时处理同一端口上的多个客户请求

23.什么是跨域?如何解决跨域问题?

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域

https://blog.csdn.net/qq_23832313/article/details/81946838

 1、端口和协议的不同,只能通过后台来解决
 2、localhost和127.0.0.1虽然都指向本机,但也属于跨域

24.java 深浅拷贝

https://blog.csdn.net/sophia__yu/article/details/87381933

(1)浅拷贝:只有子类实现Cloneable接口才可以使用Object使用的clone方法

浅拷贝是将原对象的信息拷贝,拷贝后的对象和原对象指向同一块空间。所以原对象修改之,浅拷贝的也会被修改

(2)深拷贝指:原对象的修改不会影响拷贝后的对象

利用序列化,因为序列化后:将二进制字节流内容写到一个媒介(文本或字节数组),然后是从这个媒介读取数据,原对象写入这个媒介后拷贝给clone对象,原对象的修改不会影响clone对象,因为clone对象是从这个媒介读取。

25.输入url的整个过程

https://blog.csdn.net/qq_21993785/article/details/81188253

涉及:

(1)应用层:DNS、HTTP

(2)传输层:TCP

(3)网络层:IP和路由选择协议

(4)数据链路层:ARP

过程:

(1)域名解析:根据输入的域名解析到对应的IP地址

(2)根据解析的ip地址,通过tcp三次握手建立客户端与服务器端的连接

(3)建立连接之后,发送http请求

(4)服务器处理请求,获得到html并响应给客户端

(5)浏览器解析页面并渲染

(6)四次挥手断开连接。

协议:

(1)DNS解析

  • 浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。
  • 如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址。
  • 如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。
  • 如果LDNS仍然没有命中,就直接跳到Root Server 域名服务器请求解析。
  • 根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址
  • 此时LDNS再发送请求给上一步返回的主域名服务器
  • 接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
  • Name Server根据映射关系表找到目标ip,返回给LDNS
  • LDNS缓存这个域名和对应的ip
  • LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束

(2)TCP:用于建立连接

(3)IP:在建立TCP连接时,需要发送数据,发送数据在网络层使用的就是ip协议

(4)OSPF:IP数据包在路由之间,粗腰OSPF路由选择协议。

(5)ARP:路由与服务器之间,需要需要将IP地址转换为物理地址,需要ARP协议

(6)http:

1.DNS解析到IP地址,

2.浏览器发起HTTP请求;

3.接下来到了传输层,选择传输协议,TCP或者UDP,TCP是可靠的传输控制协议,对HTTP请求进行封装,加入了端口号等信息;

4.然后到了网络层,通过IP协议将IP地址封装为IP数据报;然后此时会用到ARP协议,主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机,并接收返回消息,以此确定目标的物理地址,找到目的MAC地址;

5.接下来到了数据链路层,把网络层交下来的IP数据报添加首部和尾部,封装为MAC帧,现在根据目的mac开始建立TCP连接,三次握手,接收端在收到物理层上交的比特流后,根据首尾的标记,识别帧的开始和结束,将中间的数据部分上交给网络层,然后层层向上传递到应用层;

6.服务器响应请求并请求客户端要的资源,传回给客户端;

7.断开TCP连接,浏览器对页面进行渲染呈现给客户端。

26.状态码

https://www.sojson.com/http.html

(1)1xx(临时响应)
表示临时响应并需要请求者继续执行操作的状态代码。

100   (继续) 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。 
101   (切换协议) 请求者已要求服务器切换协议,服务器已确认并准备切换。

(2)2xx (成功)
表示成功处理了请求的状态代码。

200   (成功)  服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
201   (已创建)  请求成功并且服务器创建了新的资源。
202   (已接受)  服务器已接受请求,但尚未处理。
203   (非授权信息)  服务器已成功处理了请求,但返回的信息可能来自另一来源。
204   (无内容)  服务器成功处理了请求,但没有返回任何内容。
205   (重置内容) 服务器成功处理了请求,但没有返回任何内容。
206   (部分内容)  服务器成功处理了部分 GET 请求。

(3)(重定向)
表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

300   (多种选择)  针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
301   (永久移动)  请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302   (临时移动)  服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

303   (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
304   (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
305   (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
307   (临时重定向)  服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

(4)4xx(请求错误)
这些状态代码表示请求可能出错,妨碍了服务器的处理。

400   (错误请求) 服务器不理解请求的语法。
401   (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403   (禁止) 服务器拒绝请求。
404   (未找到) 服务器找不到请求的网页。

405   (方法禁用) 禁用请求中指定的方法。
406   (不接受) 无法使用请求的内容特性响应请求的网页。
407   (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
408   (请求超时)  服务器等候请求时发生超时。
409   (冲突)  服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
410   (已删除)  如果请求的资源已永久删除,服务器就会返回此响应。
411   (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。
412   (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。
413   (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
414   (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。
415   (不支持的媒体类型) 请求的格式不受请求页面的支持。
416   (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。
417   (未满足期望值) 服务器未满足”期望”请求标头字段的要求。

(5)5xx(服务器错误)
这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

500   (服务器内部错误)  服务器遇到错误,无法完成请求。
501   (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
502   (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。
503   (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
504   (网关超时)  服务器作为网关或代理,但是没有及时从上游服务器收到请求。
505   (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本。

27.数据库sql中 in和exists区别

https://blog.csdn.net/qq_34755766/article/details/83990289

(1)写法

//in
select * from A 
where id in(select id from B)
//exists
select a.* from A a 
where exists(select 1 from B b where a.id=b.id)

(2)查询原理

  • in:

in()只执行一次,它查出B表中的所有id字段并缓存起来.之后,检查A表的id是否与B表中的id相等,如果相等则将A表的记录加入结果集中,直到遍历完A表的所有记录.

  • exists

exists()会执行A.length次,它并不缓存exists()结果集,因为exists()结果集的内容并不重要,重要的是结果集中是否有记录,如果有则返回true,没有则返回false.

(3)使用

  • 可以看出,当B表数据较大时不适合使用in(),因为它会B表数据全部遍历一次.

如:A表有10000条记录,B表有1000000条记录,那么最多有可能遍历10000*1000000次,效率很差.
再如:A表有10000条记录,B表有100条记录,那么最多有可能遍历10000*100次,遍历次数大大减少,效率大大提升.

  • 当B表比A表数据大时适合使用exists(),因为它没有那么遍历操作,只需要再执行一次查询就行.

如:A表有10000条记录,B表有1000000条记录,那么exists()会执行10000次去判断A表中的id是否与B表中的id相等.
如:A表有10000条记录,B表有100000000条记录,那么exists()还是执行10000次,因为它只执行A.length次,可见B表数据越多,越适合exists()发挥效果

如果子查询得出的结果集记录较少,主查询中的表较大且又有索引时应该用in, 反之如果外层的主查询记录较少,子查询中的表大,又有索引时使用exists

28.泛型,和泛型的擦除

https://www.jianshu.com/p/2ad33ed2f72a

Java泛型是在JavaSE 1.5之后所采用的新特性,目的在于简化Java语言,提高Java开发的安全性与实用性。其具体的含义在于将数据类型指定为参数,而无需对数据类型进行强制转换。避免在代码运行时,系统抛出异常。应用在Java集合当中。

(1)好处

  • 类型安全:泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
  • 消除强制类型转换:泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会

举个栗子

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
}
//结果
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at test.Main.main(Main.java:18)

因为不知道对象的类型,结果会报错,但如果指定好类型,在编译期间就能提示错误,避免问题;

同时可以通过反射想一个不能添加的类型中添加。

(2)擦除

由于早期java并没有泛型这个概念,因此jvm中也没有这个类型,为了能够兼容旧的版本,所以在运行时期,会将泛型类型擦除,即,下面返回结果为true;都是List.class。在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>则类型参数就被替换成类型上限。

Java虚拟机是不存在泛型类型对象的,所有的对象都属于普通类,甚至在泛型实现的早起版本中,可以将使用泛型的程序编译为在1.0虚拟机上能够运行的class文件,这个向后兼容性后期被抛弃了,所以后来如果用Sun公司的编译器编译的泛型代码,是不能运行在Java5.0之前的虚拟机的,这样就导致了一些实际生产的问题,如一些遗留代码如何跟新的系统进行衔接,要弄明白这个问题,需要先了解一下虚拟机是怎么执行泛型代码的。

(3)擦除带来的问题

  • 子类继承父类(泛型的),当子类对父类重写时,如果类型擦除,就会导致重写变成了重载,但实际编译器并没有,为什么呢?因为采用桥接的方式添加了一个类型转换的方法。

https://blog.csdn.net/l294265421/article/details/46423133

(4)通配符

  • <? extends T>:上界通配符(Upper Bounds Wildcards)
  • <? super T>:下界通配符(Lower Bounds Wildcards)

举个栗子:

List<? extends C> list1; // list1 的元素的类型只能是 C 和 C 的子类。
List<? super C> list2; // list2 的元素的类型只能是 C 和 C 的父类。
ArrayList<B> b = new ArrayList<>();
ArrayList<C> c = new ArrayList<>();
ArrayList<D> d = new ArrayList<>();
// <? extends C>
list1 = b; // 报错
list1 = c;
list1 = d;
// <? super C>
list2 = b; 
list2 = c;
list2 = d; // 报错

29.http请求报文结构

(1)请求行:method + request-URI + http-version

(2)请求头:指浏览器或其他客户可以接爱的MIME文件格式+客户端浏览器名称+对应网址URL中的Web名称和端口号+浏览器可以接受的语言种类+Cookie等

(3)请求体:数据参数等

http响应报文结构:

(1)响应行:版本+状态码等

(2)响应头:包含服务器类型,日期,长度,内容类型等

(3)响应体:包含服务器类型,日期,长度,内容类型等

30.redis实现页面缓存

在动态生成网页的时候通常会使用模板语言来简化网页的生成操作,现在的web网页通常由头部、尾部、侧栏菜单、工具条、内容域的模板生成,有时候模板还用于生成javascript,但是对于一些不经常发生变化的页面,并不需每次访问都动态生成,对这些页面进行缓存,可以减少服务器的压力。

https://blog.csdn.net/qq_34600424/article/details/79167661

31.开发中日志:Springboot与日志

https://www.cnblogs.com/toov5/p/10745926.html

市面上的日志框架;
JUL、JCL、Jboss-logging、logbacklog4j、log4j2、slf4j...

SpringBoot:底层是Spring框架,Spring框架默认是用JCL;‘
SpringBoot选用 SLF4j和logback;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

日志的等级分为 ERROR WARN DEBUG INFO TRACE,级别从高到低

32.基本数据类型(比如int i)在内存中是怎么存的

https://blog.csdn.net/xdyong/article/details/41659711

如int a = 3;这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

33.类对象什么时候加载的

第一:生成该类对象的时候,会加载该类及该类的所有父类;

第二:访问该类的静态成员的时候;

第三:class.forName("类名");

34.static的原理

(1)用法

  • 修饰成员变量:
  • 修饰成员方法
  • 静态块

(2)由来

函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道,函数在栈上分配的空间在此函数执行结束时会释放掉,这样就产生了一个问题: 如果想将函数中此变量的值保存至下一次调用时,如何实现?最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此函数中定义的变量,不仅仅受此函数控制)。

(3)原理

静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。静态的变量和方法存储在方法区中,可以共享。在程序加载时就进行了初始化。

35.写日志类满足多线程向文件中写日志,设计一下需要实现哪些方法

https://www.cnblogs.com/xcj26/p/6037808.html

1,不管有多少线程同时需要写日志,我都用一个临时队列来存放这些日志信息。

2,再启用一个Task任务把队列的日志批量存放到.log文件里。

3,附加一个小功能,每个日志存储的大小限制,当日志太大了,查看打开的时候比较慢。

36.深克隆和浅克隆

https://blog.csdn.net/tanga842428/article/details/52516720

https://blog.csdn.net/baiye_xing/article/details/71788741

(1)浅克隆

定义

被克隆对象的所有变量都和含有原来的对象相同的值,而所有对其他对象的引用仍然指向原来的对象。即浅克隆只对要克隆的对象的属性进行克隆,而不克隆它所引用的对象

步骤:

  •  被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)
  •  覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象,(native为本地方法)

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());

	}
}
结果:
学生1:12345
学生2:12345
学生1:12345
学生2:54321

(2)深克隆

深克隆是对整个独立的对象进行克隆,包括它所有的属性和对应的引用,因此比较花费时间。

(3)例子

浅克隆的两个对象的引用指向同一个,当发生改变的时候,两个对象都会改变。

深克隆克隆所有的属性和引用对象,因此引用对象不相同

37.java多态的原理

https://blog.csdn.net/SEU_Calvin/article/details/52191321

多态的底层实现是动态绑定,即在运行时才把方法调用与方法实现关联起来。

(1)JVM静态绑定和动态绑定

  • 静态绑定:当编译时(由编译器)确定对象的类型时,它被称为静态绑定。如果在类中有任何private,final或static方法,则有静态绑定。 重载(Overload)的方法。
  • 动态绑定:当在运行时确定对象的类型时,它被称为动态绑定。类中有虚方法(可以被子类重写的方法)则会根据运行时的对象进行动态绑定。 重写(Override)的方法

(2)原理

多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同。

继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。

接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表。
 

 

 

 

 

 

 


 

 

 

 

 

 

                                                                                                                                                                                      

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
 

 

 

 

 

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值