1. static关键字是什么意思?Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?是否可以在 static 环境中访问非static 变量?
static关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java中static方法不能被覆盖,因为方法覆盖(override)是基于运行时动态绑定的,而 static 方法是编译时静态绑定的。static 方法跟类的任何实例都不相关,所以概念上不适用。
不可以在static 环境中访问非static 变量,static变量在Java中是属于类的,它在所有的实例中的值是一样的。当类被 Java 虚拟机载入的时候,会对 static 变量进行初始化(与类同生死)。非static变量,只有类被创建时,才会分配空间(如果是方法内的局部变量,那么在方法被调用的时候才创建)。
2. Java中接口和抽象类的区别
- 接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。
- 类可以实现很多个接口,但是只能继承一个抽象类。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现, 否则该类仍然需要被声明为抽象类。
- 抽象类可以在不提供接口方法实现的情况下实现接口。
- Java 接口中声明的变量默认都是final的。抽象类可以包含非final的变量。
- Java 接口中的成员函数默认是 public 的。抽象类的成员函数可以是 private,protected 或者是 public。
- 接口是绝对抽象的,不可以被实例化。抽象类也不可以被实例化,但是,如果它包含 main方法的话是可以被调用的。(抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用)
- 接口可以继承接口。抽象类可以实现(implements)接口,抽象类可继承具体类,但前提是具体类必须有明确的构造函数。
- 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
接口可以继承接口,而且支持多重继承。抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。
3. 进程和线程
- 进程是程序的一次动态执行过程,每个进程都有自己独立的内存空间。一个应用程序可以同时启动多个进程(比如浏览器可以开多个窗口,每个窗口就是一个进程),进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。
- 多进程操作系统能够运行多个进程,每个进程都能够循环利用所需要的CPU时间片,使的所有进程看上去像在同时运行一样。
- 线程是进程的一个执行流程,是CPU调度和分 派的基本单位。一个进程可以由多个线程组成,也就是一个进程可以同时运行多个不同的线程,每个线程完成不同的任务。
- 线程的并发运行:就是一个进程内若干个线程同时运行。(比如:word的拼写检查功能和首字母自动大写功能是word进程中的线程)
- 线程和进程的关系是一个局部和整体的关系,每个进程都由操作系统分配独立的内存地址空间,而同一进程的所有线程都在同一地址空间工作。(进程在执行时通常拥有独立的内存单元,而线程之间可以实现内存共享)
- 虽然使用多线程的编程通常能够带来更好的性能和用户体验,但是多线程占用更多的CPU资源,对于其他的进程并不友好(可能占用了更多的CPU 资源),也不是线程越多, 程序的性能就越好,因为线程之间的调度和切换也会浪费CPU 时间。
4. 线程的生命周期
一个线程的完整生命周期要经历5中状态:新建、就绪、运行、阻塞、死亡。
- 新建状态:使用new和某种线程的构造方法来创建线程对象,该线程就会进入新建状态,系统为该线程对象分配内存空间。处于新建状态的线程可以通过调用start()方法进入就绪状态。
- 就绪状态(Runnable):此时线程已经具备了运行的条件,进入了线程队列,等待系统分配CPU资源,一旦获得CPU资源,该线程就会进入运行状态。(线程准备运行,不一定立马就能开始执行)
- 运行状态(Running):进入运行在状态,线程会执行自己的run()方法中的代码。就是进程正在执行线程的代码。
- 阻塞状态(Blocked):一个正在执行的线程,如果执行了suspend、join或sleep方法,或等待io设备的使用权,那么该线程将会让出自己的CUP控制权并暂时中止自己的执行,进入阻塞状态。阻塞的线程,不能够进入就绪队列,只有当阻塞原因被消除的时候,线程才能进入就绪状态,重新进入线程队列中排队等待CPU资源,然后继续执行。
- 死亡状态(Dead):一个线程完成了全部工作或者被提前强制性的中止,该线程就处于死亡状态。
- 睡眠状态(Sleeping):线程通过
Thread.sleep(线程睡眠)、Object.wait(线程等待)、LockSupport.park(线程暂停)
三种方式 使线程进入休眠状态。
同步阻塞(Blocked on Synchronization):sleep( ) 使线程在一定的时间内进入阻塞状态(不会释放锁资源)、wait( ) 使线程进入阻塞状态,(释放自己占有的锁资源,搭配notify( )使用)、suspend( ) 使线程进入阻塞状态(必须其对应的resume( )被调用,才能使线程重新进入可执行状态)
5. 线程同步以及线程调度相关的方法
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM 确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
6. Thread类的sleep()方法和对象的wait()方法有什么区别?线程的sleep()方法和yield()方法有什么区别?
Thread类的sleep()方法和对象的wait()方法
sleep()*方法(休眠)是*线程类( Thread)*的*静态方法,调用此方法让当前线程暂停执行指定的时间, 将执行机会( CPU)让给其他线程, 但是
对象的锁依然保持
,因此休眠时间结束后会自动恢复(线程回到就绪状态)。
*wait()* 方法是Object 类的方法,调用对象的wait()方法导致当前线程放弃对象的锁
(线程暂停执行), 进入对象的等待池(wait pool),只有调用对象的notify()
方法(或notifyAll()
方法)时才能唤醒等待池中的线程进入等锁池(lock pool), 如果线程重新获得对象的锁就可以进入就绪状态。线程的sleep()方法和yield()方法
1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
2)线程执行sleep()方法后转入阻塞( blocked)状态, 而执行yield()方法后转入就绪( ready)状态;
3)sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
4)sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
7. 什么是线程池(thread pool)
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java 中,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是**
池化资源技术
**产生的原因。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建, 使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java 5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。工具类Executors
面提供了一些静态工厂方法,生成一些常用的线程池。
工具类Executors
生成线程池的常用方法:1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2) newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程(返回到线程池中)。【推荐使用】
3)newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
5)newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。如果希望在服务器上使用线程池,强烈建议使用newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
8. Java中守护线程和本地线程区别
java 中的线程分为两种:守护线程( Daemon)和用户线程( User)。
j任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(boolon);
true则把该线程设置为守护线程,false则设置为用户线程。Thread.setDaemon()
必须在Thread.start()之前调用,否则运行时会抛出异常。守护线程和本地线程两者的区别
唯一的区别是判断虚拟机(JVM)何时离开,守护线程Daemon是为其他线程提供服务,如果全部的用户现场Thread 已经撤离, Daemon 没有可服务的线程,JVM 撤离。也可
以理解为守护线程是JVM 自动创建的线程( 但不一定),用户线程是程序创建的线程;比如JVM 的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了,当垃圾回收线程是Java 虚拟机上仅剩的线
程时,Java 虚拟机会自动离开。
9. synchronized关键字的用法,当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
1)synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用
synchronized(对象) { }
定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符
。
2)不能进入。
其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B 方法的线程就只能在等锁池( 注意不是等待池)中等待对象的锁。
10. 简述synchronized和java.util.concurrent.locks.Lock的异同
Lock是JDK1.5 以后引入的新的API,和关键字synchronized 的异同:
相同点:Lock 能完成synchronized 所实现的所有功能;
主要不同点:Lock有比synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。
11. 死锁、活锁、饥饿?产生死锁的必要条件是什么?如何确保 N 个线程可以访问N 个资源同时又不导致死锁?
- 死锁、活锁、饥饿 的概念,之间的区别
1)死锁:两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程都陷入了无限的等待中。因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去
2)活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试, 失败。
3)饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。活锁和死锁的区别:
处于活锁的实体是在不断的改变状态,所谓的“ 活”, 而
处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。Java 中导致饥饿的原因:
1)高优先级线程吞噬所有的低优先级线程的CPU 时间。
2)线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前
持续地对该同步块进行访问。
3)线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait 方
法),因为其他线程总是被持续地获得唤醒。
- 产生死锁的必要条件:
1)互斥条件:所谓互斥就是进程在某一时间内独占资源。
2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3)不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
- 如何确保 N 个线程可以访问N 个资源同时又不导致死锁
使用多线程的时候,一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
多线程的上下文
多线程会共同使用一组计算机上的CPU,而线程数大于给程序分配的CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用CPU。不同的线程切换使用CPU发生的切换数据等就是上下文切换。
12. 内存中的栈(stack)、堆(heap)和方法区(method area)的用法
- 堆:存放方法对象(通过new 关键字和构造器创建的对象)
- 栈:存放方法以及方法中的局部变量
- 方法区:(也叫共享区)存放代码片段、静态属性、常量池(比如常量池中存放字符串的值)
通常我们定义一个基本数据类型的变量,一个对象的引用,函数调用的现场保存都使用JVM 中的栈空间;而通过new 关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为Eden、Survivor(又可分为From Survivor 和To Survivor)、Tenured;方法区和堆都
是各个线程共享的内存区域,用于存储已经被JVM 加载的类信息、常量、静态变
量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的100、”
hello” 和常量都是放在常量池中, 常量池是方法区的一部分。栈空间操作起来
最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM
的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量
池空间不足则会引发OutOfMemoryError。String str = new String("HelloWorld");
,这个代码中,变量str 存放在栈上,用new 创建出来的字符串对象放在堆上,”HelloWorld” 这个字面量(String)是放在方法区中。
13. switch 的参数类型
JDK1.5 以前,switch()的参数中,只能是
byte、short、char、int
。
从JDK1.5 开始, Java 中引入了枚举类型与byte short char int的包装类。
从JDK 1.7 开始,参数还可以是字符串( String)类型,但是长整型( long)在目前所有的版本中都是不可以的。
也就是现在switch支持byte、short、char、int、String、枚举
JDK1.5,对四个包装类的支持是因为java编译器在底层手动进行拆箱,而对枚举类的支持是因为枚举类有一个ordinal方法,该方法实际上是一个int类型的数值。
jdk1.7开始支持String类型,但实际上String类型有一个hashCode算法,结果也是int类型.而byte short char类型可以在不损失精度的情况下向上转型成int类型,所以总的来说,可以认为switch中只支持int.
14. 两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?【***】
不对,如果两个对象x 和y 满足x.equals(y) == true,它们的哈希码(hash code)
应当相同。
Java 对于eqauls 方法和hashCode 方法是这样规定的:1)如果两个对象相同(equals 方法返回true),那么它们的hashCode 值一定要相同;
2)如果两个对象的hashCode 相同,它们并不一定相同。当然,你未必要按照要求去做,但是(重写hashCode)如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。关于equals方法,有以下内容必须满足
首先equals 方法必须满足
1)自反性( x.equals(x)必须返回true)
2)对称性( x.equals(y)返回true 时, y.equals(x)也必须返回true)
3)传递性(x.equals(y)和y.equals(z)都返回true 时, x.equals(z)也必须返回true)
4)一致性(当x和y引用的对象信息没有被修改时,多次调用x.equals(y)应该得到同样的返回值) ,而且对于任何非null值的引用x,x.equals(null)必须返回false。实现高质量的equals 方法的诀窍包括
1)使用==操作符检查”参数是否为这个对象的引用”;
2)使用instanceof 操作符检查”参数是否为正确的类型”;
3) 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;
4)重写equals方法后,问自己它是否满足自反性、对称性、传递性、一致性;
5) 重写equals 时总是要重写hashCode;
6) 不要将equals 方法参数中的Object 对象替换为其他的类型,在重写时不要忘掉@Override 注解。
15. String 和StringBuilder、StringBuffer 的区别【***】
Java平台提供了两种类型的字符串: String 和StringBuffer/StringBuilder,它们可以储存和操作字符串。
其中String 是只读字符串,也就意味着String 引用的字符串内容是不能被改变的。
而StringBuffer/StringBuilder类表示的字符串对象可以直接进行修改。StringBuilder 是JDK 1.5 中引入的,它和StringBuffer 的方法完全相同, 区别在于StringBuffer因为被synchronized 修饰,是线程安全的而StringBuilder是线程不安全的(所有方面都没有被synchronized 修饰)
因为StringBuilder没有被synchronized同步修饰,所以StringBuilder【面试题】什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder 对象的append 方法连接字符串性能更好?
首先要清楚以下两点:
1)String 对象的intern方法会得到字符串对象在常量池中对应的版本的引用(如果常量池中有一个字符串与String 对象的equals结果是true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中, 然后返回常量池中字符串的引用;
2)字符串的+操作其本质是创建了StringBuilder对象进行append操作,然后将拼接后的StringBuilder 对象用toString方法处理成String 对象。
16. 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,而重写实现的是运行时的多态性。
所谓的重载是编译时多态性,就是根据实际的参数列表,在编译的时候就能够确定执行重载方法中的哪一个了。
所谓的重写是运行时多态性,就比如父类对象引用子类实例时,调用方法只有在运行时才知道到底执行了哪个方法。
重载发生在一个类中,同名的方法如果有不同的参数列表( 参数类型不同、参数个数不同或者二者都不同)则视为重载;(重载对返回类型没有特殊的要求,只考虑参数列表)
重写发生在子类与父类之间(需要继承),重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常( 里氏代换原则)。
17. JVM是什么?为什么 Java 被称作是“平台无关的编程语言”?JVM 加载class 文件的原理机制【***】
Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。
Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
Java代码在JVM中的执行流程
JVM的类加载原理图
JVM 中类的装载是由类加载器(ClassLoader
)和它的子类来实现的, Java 中的类加载器是一个重要的Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java 的跨平台性,经过编译的Java 源程序并不是一个可执行程序,而是一个或多个类class文件。当Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接( 验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备( 为静态变量分配内存并设置默认的初始值)和解析( 将符号引用替换为直接引用)三个步骤。最后JVM 对类进行初始化,包括:
1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
2)如果类中存在初始化语句, 就依次执行这些初始化语句。类的加载是由类加载器完成的
类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器( System)和用户自定义类加载器(java.lang.ClassLoader 的子类) 。
从Java 2( JDK 1.2)开始, 类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了Java 平台的安全性,在该机制中, JVM 自带的Bootstrap 是根加载器, 其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向Java 程序提供对Bootstrap 的引用。
Bootstrap
:一般用本地代码实现,负责加载JVM 基础核心类库(rt.jar
);Extension
:从java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;System
:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath 或者系统属性java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
18. Java 中会存在内存泄漏吗?
理论上,Java因为有垃圾回收机制( GC)不会存在内存泄露问题( 这也是Java 被广泛使用于服务器端编程的一个重要原因);
然而在实际开发中,可能会存在无用但可达的对象
,这些对象不能被GC 回收,因此也会导致内存泄露的发生。
例如:Hibernate 的Session( 一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空( flush)一级缓存就可能导致内存泄露。
//实现了一个栈(先进后出(FILO))结构代码
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if(size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
123456789101112131415161718192021222324252627282930
上述代码是实现了一个栈的先进后出(FILO)结构,但是其中的pop 方法却存在
内存泄露的问题
当我们用pop 方法弹出栈中的对象时,该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象, 因为栈内部维护着对这些对象的过期引用(obsolete reference)。
在支持垃圾回收的语言中,内存泄露是很隐蔽的,这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了,那么垃圾回收器不会处理这个对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也可能会导致很多的对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发Disk Paging( 物理内存与硬盘的虚拟内存交换数据),甚至造成OutOfMemoryError。
19. 抽象方法(abstract)是否可以被静态的(static)、本地方法(native)和同步(synchronized)修饰。
答案是都不能。
1)抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
2)本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。
3)synchronized 和方法的实现细节有关,抽象方法不涉及实现细节, 因此也是相互矛盾的。
20. 静态变量和实例变量的区别
静态变量是被static修饰符修饰的变量,也称为类变量, 它属于类(与类同生死),不属于类的任何一个对象,一个类不管创建多少个对象, 静态变量在内存中有且仅有一个拷贝;静态变量可以实现让多个对象共享内存。
实例变量必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。
(在Java 开发中, 上下文类和工具类中通常会有大量的静态成员。)
21. 实现对象克隆的2种方法【***】
对象是引用数据类型,对象的赋值仅仅是吧对象的引用赋值给另一个对象,他们的堆空间是相同的。
克隆就是需要有一个完全相同的对象,而且两者之间完全互不影响,克隆对象和原对象是两个完全对立的对象。(java.lang.Object类的clone()方法,对象克隆就是对象的复制,即完整的复制出一个对象。)1)实现
Cloneable
接口并重写Object 类中的clone()方法;(浅克隆对象的克隆默认是浅度克隆,也就是包含在对象内部的对象是不会被克隆的),【通过clone()实现深度克隆:让克隆对象所包含的类也实现clone功能(实现cloneable接口,重载clone()方法)】
2)实现Serializable
接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆基于序列化和反序列化(
Serializable
)实现的克隆不仅仅是深度克隆, 更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object 类的clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
22. Java 中如何实现序列化Serializable,有什么意义?
Serializable序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
序列化是为了解决对象流读写操作时可能引发的问题( 如果不进行序列化可能会存在数据乱序的问题) 。
要实现序列化,需要让一个类实现Serializable
接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);
如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject 方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(上面所说的)。
23. GC垃圾回收器,为什么需要它。【***】
java中由JVM的垃圾回收管理器(GC)来负责回收不需要再使用的堆内存和栈内存。GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。
JVM的垃圾回收采用动态存储管理技术,它能够自己检测内存的使用情况,自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现内存资源自动回收功能。
JVM垃圾回收管理器处理释放没用的对象外,还可以清除内存记录的碎片(因为创建对象和垃圾收集齐全不断释放对其对象所占用的空间,因此内存会出现碎片)。碎片整理将所占用的堆内存移动到堆的一端,JVM将整理出的内存分配给新的对象。
JVM垃圾回收器有一个潜在的缺点 : 增加了系统的开销,影响了程序的性能。因为JVM必须追踪运行程序中有用的对象,到最终释放没用的对象,这个过程需要花费处理器的时间。
一个对象如果不再被任何栈内存所引用,则该对象成为垃圾对象。垃圾收集器对垃圾对象的收集时间是不确定的,也可以直接使用System.gc()
方法Runtime.getRuntime().gc()
回收垃圾对象,但是这种强制执行垃圾回收程序对系统性能会产生负面影响。(JVM 可以屏蔽掉显示的垃圾回收调用,就是显示调用方法,JVM的GC会自动进行管理)虽然你可以调用System.gc() 或者Runtime.gc(),但是没有办法保证GC的执行。垃圾回收可以有效的防止内存泄露, 有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回
收器对某个对象或所有对象进行垃圾回收。在Java 诞生初期,垃圾回收是Java
最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过
境迁,如今Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常
觉得iOS 的系统比Android 系统有更好的用户体验,其中一个深层次的原因就在
于Android 系统中垃圾回收的不可预知性。补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量
垃圾回收等方式。标准的Java 进程既有栈又有堆。栈保存了原始型局部变量,堆
保存了要创建的对象。Java 平台对堆内存回收和再利用的基本算法被称为标记和
清除,但是Java 对其进行了改进,采用“ 分代式垃圾收集”。这种方法会跟Java
对象的生命周期将堆内存划分为不同的区域, 在垃圾收集过程中,可能会将对象
移动到不同区域:
24. String和基本数据类型之间的转换
1)将字符串转换为基本数据类型
调用基本数据类型对应的包装类中的方法parseXXX(String)或valueOf(String)
即可返回相应基本类型;
2)将基本数据类型转换为字符串
一种方法是将基本数据类型与空字符串""
通过+
连接即可获得其所对应的字符串;
另一种方法是调用String 类中的valueOf()方法返回相应字符串。String.valueOf()
25. Java和JavaScript区别
Java和JavaScript介绍
JavaScript 与Java 是两个公司开发的不同的两个产品。Java 是原Sun Microsystems 公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript 是Netscape 公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web 页面中运行的基于对象和事件驱动的解释性语言。
JavaScript 的前身是LiveScript;而Java 的前身是Oak 语言。Java和JavaScript的区别
1)Java是面向对象的而JavaScript是基于对象的。
Java 是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript 是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
2)Java是编译运行,而JavaScript是解释执行的。
Java 的源代码在执行之前,必须经过编译。JavaScript 是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript 的运行效率
3)Java变量必须先声明后使用,JavaScript是弱类型语言,直接使用。
Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript 中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript 的解释器在运行时检查推断其数据类型。
4)Java是静态语言,JavaScript是动态语言。
26. try{}里有一个return 语句,finally是否执行,执行位置。
try{}里有一个return语句返回,那么紧跟在这个try后的finally{}里的代码也是会被执行的,是在方法返回调用前(return前执行)
如果存在finally代码块,try中的return语句不会立马返回调用者,而是记录下返回值待finally代码块执行完毕之后再向调用者返回其值(执行return)。
在finally中改变返回值的做法是不好的,如果在finally 中修改了返回值,就会返回修改后的值。在finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误(idea、eclipse也能自己设置)
27. 运行时异常和受检查异常
异常表示程序运行过程中可能出现的非正常状态。
运行时异常:表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。(在运行是抛出)
受检查异常跟程序运行的上下文环境有关,即使程序设计无误, 仍然可能因使用的问题而引发。Java 编译器要求方法必须声明抛出可能发生的受检查异常,但是并不要求必须声明抛出未被捕获的运行时异常。
常见的运行时异常:ArithmeticException(算术异常)
ClassCastException (类转换异常)
IllegalArgumentException (非法参数异常)
IndexOutOfBoundsException (下标越界异常)
NullPointerException (空指针异常)
SecurityException (安全异常)
28. final、finally、finalize 的区别
final关键字
一个基本数据类型声明为final,就只能进行一次赋值(初始化),编译器会自动检查代码,如果需要修改final的初始化,就会编译报错。final声明的引用数据类型,当前引用是不能改变的,但是可以改变引用指向的内存空间的值。final一般和static搭配使用作为常量。
final关键字的三种用法:
1)修饰类:表示该类不能被继承;
2)修饰方法:表示方法不能被重写;
3)修饰变量: 表示变量只能一次赋值以后值不能被修改(常量)(final修饰成员变量必须在声明时初始化或者再构造器中初始化,否则报编译错误,而且final修饰的变量通常都是常量,常量名全部大写)
如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract 是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final 的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final 的方法也同样只能使用,不能在子类中被重写。final优势:
1)final关键字可以提高性能,JVM和Java应用都会缓存final变量。
2)final变量可以在安全的多线程环境下进行资源共享,而不需要额外的同步开销。finally
通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM 不关闭都能执行,可以将释放外部资源的代码写在finally 块中(关闭数据库连接、释放资源)。
finalize
Object 类中定义的方法,Java 中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。
29. List、Map、Set 三个接口存取元素时,各有什么特点?
List以特定索引来存取元素,可以有重复元素。
Set 不能存放重复元素(用对象的equals()*方法来区分元素是否重复)。
**Map**保存*键值对( key-value)映射,映射关系可以是一对一或多对一。(Map不支持一对多,但是可以用Map<Integer, List>
这种格式来达到一对多的系,多对多类似)Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键( key)构成排序树从而达到排序和去重的效果。
30. ArrayList、Vector、LinkedList 的存储性能和特性【***】
ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引查询数据快而插入数据慢,
Vector 中的方法由于添加了synchronized 修饰,因此Vector 是线程安全的容器,但性能上较ArrayList 差,因此已经是Java 中的遗留容器。
LinkedList 使用双向链表
实现存储( 将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比, 内存的利用率更高) ,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
Vector 属于遗留容器(Java 早期的版本中提供的容器, 除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用, 但是由于ArrayList和LinkedListed都是非线程安全的
, 如果遇到多个线程操作同一个容器的场景,则可以通过工具Collections 中的synchronizedList 方法将其转换成线程安全的容器后再使用( 这是对装潢模式的应用, 将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
31. Collection 和Collections 的区别,Collections 工具类中的sort()方法如何比较元素,TreeMap和TreeSet 在排序时如何比较元素
Collection是一个接口, 它是Set、List 等容器的父接口。
Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。Collections工具类的sort方法有两种重载的形式,第一种要求传入
的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较, 但是要求传入第二个参数, 参数是Comparator接口的子类型(需要重写compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法, 也是对回调模式的应用( Java 中对函数式编程的支持)。TreeSet要求存放的对象所属的类必须实现
Comparable
接口,该接口提供了比较元素的compareTo()
方法,当插入元素时会回调该方法比较元素的大小。
TreeMap要求存放的键值对映射的键必须实现Comparable 接口从而根据键对元素进行排序。
32. XML 文档定义有几种形式?它们之间有何本质区别?解析XML 文档有哪几种方式?
1)XML文档定义分为DTD和Schema两种形式,二者都是对XML语法的约束。
2)DTD和Schema两种形式的本质区别在于Schema本身也是一个XML文件,可以被XML 解析器解析,而且可以为XML 承载的数据定义类型,约束能力较之DTD更强大。
3)对XML的解析主要有DOM(文档对象模型,Document Object Model)、SAX( Simple API forXML)和StAX(JDK1.6 中引入的新的解析XML的方式,Streaming API for XML)。其中DOM处理大型文件时其性能下降的非常厉害,这个问题是由DOM 树结构占用的内存较多造成的,而且DOM 解析方式必须在解析文件之前把整个文档装入内存,适合对XML 的随机访问( 典型的用空间换取时间的策略);
SAX是事件驱动型的XML解析方式,它顺序读取XML 文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过事件回调代码来处理XML文件,适合对XML 的顺序访问;顾名思义,
StAX 把重点放在流上,实际上StAX与其他解析方式的本质区别就在于应用程序能够把XML作为一个事件流来处理。SAX 也是这样做的,但不同之处在于StAX 允许应用程序代码把这些事件逐个拉出来,而不用提供在解析器方便时从解析器中接收事件的处理程序。
33. XML的主要作用
XML的主要作用有两个方面:数据交换和信息配置。
在做数据交换时,XML将数据用标签组装成起来, 然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理,XML曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被JSON( JavaScript Object Notation)取而代之。当然,目前很多软件仍然使用XML 来存储配置信息,我们在很多项目中
通常也会将作为配置信息的硬代码写在XML 文件中,Java 的很多框架也是这么做的, 而且这些框架都选择了dom4j 作为处理XML 的工具,(因为Sun 公司的官方API 实在不怎么好用。)
34. JDBC操作数据库的步骤(MySQL)
//1. 加载JDBC驱动程序(加载MySQL驱动类)
Class.forName("com.mysql.jdbc.Driver");
//2. 提供JDBC连接的URL来创建连接
//databaseName数据库名称,useUnicode=true:表示使用Unicode字符集,characterEncoding=UF-8字符编码方式utf-8, udrtnsmr和password是mysql连接用户名和密码
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/databaseName?useUnicode=true&characterEncoding=UF-8;",username,password);
// 也可以将mysql驱动写到DriverManager.getConnection()中
//3. PreparedStatement预编译sql
String sql = "select * from dept where id = ? and name = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setInt(1, 10);
ps.setInt(2, "研究员");
//4. 执行SQL语句
ResultSet rs = ps.executeQuery();
//5. 处理结果
while(rs.next()) {
System.out.println(rs.getInt("id"));
System.out.println(rs.getInt("name"));
}
//6. 在finally里面进行释放资源(关闭外部资源的顺序应该和打开的顺序相反)
if(rs !=null){
rs.close;
}
if(ps != null){
ps.close();
}
if(con != null) {
con.close();
}
1234567891011121314151617181920212223242526272829
35. Statement和PreparedStatement哪个性能更好?
PreparedStatement性能更好
1)PreparedStatement接口代表预编译的语句,它主要的优势在于可以减少SQL 的编译错误并增加SQL的安全性(减少SQL 注射攻击的可能性)
2) PreparedStatement 中的SQL 语句是可以带参数的,避免了用字符串连接拼接SQL 语句的麻烦和不安全;
3)当批量处理SQL 或频繁执行相同的查询时,PreparedStatement 有明显的性能上的优势,由于数据库可以将编译优化后的SQL 语句缓存起来,下次执行相同结构的语句时就会很快(不用再次编译和生成执行计划)。
36. JDBC 能否处理Blob 和Clob
Blob
是指二进制大对象(Binary Large Object)Clob
是指大字符对象(Character Large Objec)
Blob是为存储大的二进制数据而设计的,而Clob 是为存储大的文本数据而设计的。JDBC 的PreparedStatement和
ResultSet都提供了相应的方法来支持Blob和Clob操作(流的操作)。
37. 数据库编程时,连接池的优势
由于创建连接和释放连接都有很大的开销(尤其是数据库服务器不在本地时,每次建立连接都需要进行TCP的三次握手,释放连接需要进行四次挥手,造成很大的开销),为了提升系统访问数据库的性能,可以事先创建若干连接置于连接池中,需要时直接从连接池获取, 使用结束时归还连接池而不必关闭连接,从而避免频繁创建和释放连接所造成的开销,这是典型的用空间换时间的策略(浪费了空间存储连接,但节省了创建和释放连接的时间)。
池化技术在Java 开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于Java 的开源数据库连接池主要有:C3P0、Proxool、DBCP、BoneCP、Druid
等。
其实大型网站性能优化的一个关键就是使用缓存,,而缓
存和连接池非常类似, 也是使用空间换时间的策略。可以将热点数据置于缓存中,当用户查询这些数据时可以直接从缓存中得到, 避免频繁的访问数据库造成大量的开销(现在主要用Redis
实现缓存)
38. Java分层,Dao模式是什么?
DAO( Data Access Object)是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中,应该将所有对数据源的访问操作进行抽象化后封装在一个公共API 中。
用程序设计语言来说, 就是建立一个接口,接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中,当需要和数据源进行交互的时候则使用这个接口,并且编写一个单独的类来实现这个接口,在逻辑上该类对应一个特定的数据存储。DAO 模式实际上包含了两个模式,一是Data Accessor(数据访问器),二是Data Object(数据对象),前者要解决如何访问数据的问题,而后者要解决的是如何用对象封装数据。
39. 事务的特性ACID,事务隔离级别,事务的并发问题【****】
- 事务具有ACID四个特性:
1)原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生
2)一致性(Consistency):事务在完成后数据的完整性必须保持一致
3)隔离性(Isolation):多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务之间的数据要相互隔离
4)持久性(Durability):一个事务一旦被提交,它对数据库中数据的改变应该是永久性的,即使数据库发生故障也不应该对其有任何影响如果整个事务执行过程中,有任何一个地方出现异常/错误,那么都会进行事务回滚,回滚之后数据的状态将和事务执行之前完全一致。
- 事务的隔离级别:
数据库为用户提供了自动锁机制,只要用户指定会话的事务隔离级别, 数据库就会通过分析SQL语句然后为事务访问的资源加上合适的锁。
隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义有五个表示隔离级别的常量(用于解决并发问题)一般情况下使用中间两种就行。
TransactionDefinition 接口事务隔离级别 | 描述 |
---|---|
READ_UNCOMMITTED | 该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。 |
READ_COMMITTED | 系统默认值,该隔离级别表示一个事务只能读取另一个事务已经提交的数据。可以防止脏读,是大多数情况下的推荐值。 |
REPEATABLE_READ | 该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。 |
SERIALIZABLE | 所有的事务依次逐个执行,事务之间就完全不可能产生干扰。该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 |
隔离级别(√:允许出现 ×:不允许出现) | 脏读 | 不可重复读 | 幻读 | 第一类丢失更新 | 第二类丢失更新 |
---|---|---|---|---|---|
READ_UNCOMMITTED | √ | √ | √ | × | √ |
READ_COMMITTED | × | √ | √ | × | √ |
REPEATABLE_READ | × | × | √ | × | × |
SERIALIZABLE | × | × | × | × | × |
事务隔离级别和数据访问的并发性是对立的,事务隔离级别越高并发性就越差。所以要根据具体的应用来确定合适的事务隔离级别,这个地方没有万能的原则。
- 事务的并发问题
首先要知道,只有存在并发数据访问时才需要事务(提交/回滚就结束事务),当多个事务访问同一数据时,可能会存在5类并发问题,包括3 类数据读取问题( 脏读、不可重复读和幻
读和2类数据更新问题(第1 类丢失更新和第2 类丢失更新)
3类数据读取问题1)脏读(Dirty Read): 一个事务读到了另一个事务的还没有提交数据. (比如A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A 读取到的数据就是脏数据。)(很严重的行为,必须处理,不然可能有很大的影响,比如转账事务)
2)不可重复读(Unrepeatable Read): 一个事务中多次读到的数据不一致,一个事务读到了另一个事务修改后的数据。(不可重复读,保证数据修改后,不会出现两次一样的数据)
3) 幻读(虚读Phantom Read):一个事务读到了另一个事务insert提交的数据。(比如事务A 重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务B 提交的行)(不可能出现在MySQL中,只会出现在Oracle中)两类丢失更新问题
1)第一类丢失更新:
事务A撤销时, 把已经提交的事务B的更新数据覆盖了(比如 取款事务A开启事务查询余额1000元,转账事务B开启事务转账100给A,A取出100,提交事务之后,再查询余额还是1000元)
2)事务A覆盖事务 已经提交的数据,造成事务B 所做的操作丢失(比如:取款事务A和转账B先后开启事务,先后查询余额都是1000元,取款事务A取出100,余额变成900,提交事务,但是此时转账事务B存入100,将余额修改为1100元,提交事务,然后再查询帐户余额就是1100,取款事务A的操作丢失)
JDBC如何进行事务处理:
Connection
提供了事务处理的方法,通过调用setAutoCommit(false)
可以设置手动提交事务。当事务完成后用commit()
显式提交事务;如果在事务处理过程中发生异常则通过rollback()
进行事务回滚。除此之外, 从JDBC 3.0 中还引入了Savepoint( 保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
40. 正则表达式是什么,Java中如何支持正则表达式。
在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说, 正则表达式就是记录文本规则的代码。正则表达式就是在进行字符串匹配和处理的时候最为强大的工具,绝大多数语言都提供了对正则表达式的支。
Java中的String类提供了支持正则表达式操作的方法,包括:matches()、replaceAll()、replaceFirst()、split()
此外,Java 中可以用Pattern类表示正则表达式对象, 它提供了丰富的API 进行各种正则表达式操作。
41. 获取Class对象的三种方法,通过反射创建对象的方法
获取class对象的三种方法:
1)每个类通过class属性获取。【
类名.class
】
2) 每个对象通过getClass()方法获取。【对象.getClass()
】
3)通过Class.forName(“类的完整名”)获取,需要捕获异常。【Class.forName("类的完整名")
】第一种方式,
类名.class
不会将类加载到内存,第三种Class.forName()
会将类加载到内存,对象.getClass()(创建对象了)会加载到内存中
通过反射创建对象的两种方法:
1)通过类对象(class)调用1newInstance()1方法,例如创建String对象:
String.class.newInstance()
2)通过类对象(class)的getConstructor()getDeclaredConstructor()
方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:
·String.class.getConstructor(String.class).newInstance(“Hello”);·
42. 23种经典设计模式
类型 | 设计模式 |
---|---|
创建型 | 工厂方法模式(FactoryMethod)、抽象工厂模式(AbstractFactory)、建造者模式(Builder)、原型模式(Prototype)、单例模式(Singleton) |
结构型 | 适配器模式(Adapter)、桥接模式(Bridge)、组合模式(Composite)、装饰器模式(Decorator)、门面模式(Facade)、享元模式(Flyweight)、代理模式(Proxy) |
行为型 | 解释器模式(Interpreter)、模板方法模式(TemplateMethod)、责任链模式(ChainofResponsibility)、命令模式(Command)、迭代器模式(Iterator)、调解者模式(Mediator)、备忘录模式(Memento)、观察者模式(Observer)、状态模式(State)、策略模式(Strategy)、访问者模式(Visitor) |
单例模式:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。(懒汉式、饿汉式写法)
原型模式:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
策略模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
工厂模式:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。工厂类可以根据条件生成不同的子类实例,这些子类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作(多态方法)。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
代理模式:给一个对象提供一个代理对象,并由代理对象控制原对象的引用。实际开发中,按照使用目的的不同,代理可以分为:远程代理、虚拟代理、保护代理、Cache 代理、防火墙代理、同步化代理、智能引用代理。
适配器模式:把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起使用的类能够一起工作。
模板方法模式:提供一个抽象类,将部分逻辑以具体方法或构造器的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法(多态实现),从而实现不同的业务逻辑。
状态模式:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
装饰者模式:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
了解其他的可以从这里面看:23种设计模式详解
43. 面向对象的七大设计原则
原则 | 描述 |
---|---|
开闭原则(OCP) | 软件实体应当对扩展开放,对修改关闭 |
里氏替换原则(LSP) | 阐述了有关继承的一些原则(什么时候使用继承),里氏替换原是继承复用的基础,反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。可以这么理解:子类可以扩展父类的功能,但不能改变父类原有的功能,也就是子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。 |
依赖倒置原则(DIP) | 核心思想是:要面向接口编程,不要面向实现编程。(抽象层相对稳定,因此以抽象为基础搭建起来的架构要比以细节(实现类)为基础搭建起来的架构要稳定得多) |
单一职责原则(SRP) | 提出对象不应该承担太多职责。单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。 |
接口隔离原则(ISP) | 要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。接口要小而专,绝不能大而全。 |
迪米特法则(LoD) | 又叫作最少知识原则(LKP),一个对象应当对其他对象有尽可能少的了解。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。 |
合成复用原则(CRP) | 叫组合/聚合复用原则(CARP)要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。 |
概述:
开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
单一职责原则告诉我们实现类要职责单一;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合度;
合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。
44. UML概念,常用UML图
UML是统一建模语言( Unified Modeling Language)的缩写,它发表于1997年, 综合了当时已经存在的面向对象的建模语言、方法和过程, 是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段提供模型化和可视化支持。使用UML 可以帮助沟通与交流,辅助应用设计和文档的生成,还能够阐释系统的结构和行为。
常用UML图
用例图(use case diagram)、类图(class diagram)、时序图(sequencediagram)、协作图(collaboration diagram)、状态图(statechart diagram)、活动图(activity diagram)、构件图(component diagram)、部署图(deploymentdiagram)
用例图:用来捕获需求,描述系统的功能,通过该图可以迅速的了解系统的功能模块及其关系
类图:描述类以及类与类之间的关系,通过该图可以快速了解系统
时序图:描述执行特定任务时对象之间的交互关系以及执行顺序, 通过该图可以了解对象能接收的消息也就是说对象能够向外界提供的服务。
45. HashMap实现原理
JDK1.8中,HashMap采用
位桶+链表+红黑树
实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
HashCode是jdk根据对象的地址或字符串或者数字利用hash算法计算出的int类型的数值。
HashMap实现原理:首先有一个每个元素都是链表的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
在jdk8中,HashMap处理“碰撞”增加了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特点是查询时间是O(logn))存储(有一个阀值控制,大于阀值(8个),将链表存储转换成红黑树存储)红黑树是一种自平衡二叉查找树
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。
对于任何有效的红黑树都有以下要求:
1)节点是红色或黑色。
2)根节点是黑色。
3)每个叶节点是黑色的。
4)每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
46. 同步和异步
如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源), 例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时, 就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
事实上,所谓的同步就是指阻塞式操作, 而异步就是非阻塞式操作。(同步必须等待返回结果才能继续执行,异步就是浏览器发送请求,不管服务器是否返回结果,都可以继续执行)
47. Servlet的生命周期
Web容器加载Servlet并将其实例化后,Servlet生命周期开始,容器运行其init()方法进行Servlet的初始化;请求到达时调用Servlet的service方法,service方法会调用与请求对应的doGet或doPost等方法;当服务器关闭会项目被卸载时服务器会将Servlet实例销毁,此时会调用Servlet的destroy方法。
48. get和post请求的区别
1)get请求用来从服务器上获得资源,而post是用来向服务器提交数据
2)get将表单中数据按照name=value的形式,添加到action 所指向的URL 后面,并且两者使用“?”连接,而各个变量之间使用“&”连接;post是将表单中的数据放在HTML头部(header),传递到action所指向URL
3)get传输的数据要受到URL长度限制(1024字节);而post可以传输大量的数据,上传文件只能使用post方式
4)使用get时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用get;对于敏感数据还是应用使用post
49. HttpServlet容器响应Web客户请求流程?
1)Web客户向Servlet容器发出Http请求;
2)Servlet容器解析Web客户的Http请求;
3)Servlet容器创建一个HttpRequest对象,在这个对象中封装Http请求信息;
4)Servlet容器创建一个HttpResponse对象;
5)Servlet容器调用HttpServlet的service方法,这个方法中会根据request的Method来判断具体是执行doGet还是doPost,把HttpRequest和HttpResponse对象作为service方法的参数传给HttpServlet对象;
6)HttpServlet调用HttpRequest的有关方法,获取HTTP请求信息;
7)HttpServlet调用HttpResponse的有关方法,生成响应数据;
8)Servlet容器把HttpServlet的响应结果传给Web客户
50. 什么是Callable 和Future?
Callable 接口类似于Runnable,但是Runnable 不会返回结果,并且无法抛出返回结果的异常,而Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。可以认为是带有回调的Runnable。
Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说Callable用于产生结果,Future 用于获取结果。
JAVA一些题目和Java的一些方法
- Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
Math.round(11.5)的返回值是12, Math.round(-11.5)的返回值是-11。四舍五
入的原理是在参数上加0.5 然后进行下取整。
short s1 = 1; s1 = s1 + 1;
有错吗?short s1 = 1; s1 += 1;
有错吗?
1)
short s1 = 1; s1 = s1 + 1;
编译错误。由于1 是int 类型,因此s1+1运算结果也是int型, 需要强制转换类型才能赋值给short 型。
2)short s1 = 1; s1 += 1;
编译正确,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换(自增长自动转换,除非越界)
- 用最有效率的方法计算2乘以8(运算符
<<
和>>
的概念)
2 << 3
(左移3 位相当于乘以2 的3 次方,右移3 位相当于除以2 的3 次方。关于运算符
<<
和>>
x >> n
就是 先将x转成二进制,不读后面的n位x << n
就是 先将x转成二进制,往二进制数据后面加n个0案例:
int x = 16;
x >> 1
输出x=8(先将x转成二进制 10000, 不读最后一位0, 输出 1000, 转为10进制即为8)x << 1
输出x=32(先将x转成二进制 10000,,往最后再读取一位0( 或根据是否已经有移位), 输出 100000, 转为10进制即为32)
- 数组有没有length()方法?String 有没有length()方法?
数组没有length()方法,只有有length 的属性。String 有length()方法。但是在JavaScript中 ,获得字符串的长度是通过length 属性得到的(非常容易和Java搞混)
- 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调
用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和C#中可以
通过传引用或传输出参数来改变传入的参数的值。在C#中可以编写如下所示的代
码, 但是在Java 中却做不到。
String str = new String(“xyz”);
创建了几个字符串对象?
两个对象,一个是静态区(方法区常量池)的”xyz“字符串,一个是用new 创建在堆上的对象str。
- Java 中有几种类型的流,它们的关系
Java中主要是字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。
在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便
关于Java 的I/O 需要注意的有两点:1)两种对称性( 输入和输出的对称性,字节和字符的对称性)
2)两种设计模式(适配器模式和装潢模式)。
- 编程实现文件拷贝的两种方式
public static void fileCopy(String source, String target) throws
IOException {
try (InputStream in = new FileInputStream(source)) {
try (OutputStream out = new FileOutputStream(target)) {
byte[] buffer = new byte[4096];
int bytesToRead;
while((bytesToRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
}
}
}
123456789101112
NIO方式
public static void fileCopyNIO(String source, String target) throws
IOException {
try (FileInputStream in = new FileInputStream(source)) {
try (FileOutputStream out = new FileOutputStream(target)) {
FileChannel inChannel = in.getChannel();
FileChannel outChannel = out.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(4096);
while(inChannel.read(buffer) != -1) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
}
}
}
12345678910111213141516
- 写一个方法,输入一个文件名和一个字符串,统计这个字符串在这个文件中出现的次数。
//统计这个字符串在这个文件中出现的次数
public static int countWordInFile(String filename, String word) {
int counter = 0; //统计数
try (FileReader fr = new FileReader(filename)) {
try (BufferedReader br = new BufferedReader(fr)) {
String line = null;
while ((line = br.readLine()) != null) {
int index = -1;
while (line.length() >= word.length() && (index = line.indexOf(word)) >= 0) {
counter++;
line = line.substring(index + word.length());
}
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
return counter;
}
12345678910111213141516171819
- 如何用Java 代码列出一个目录下所有的文件?
1)只列出当前文件夹下的文件
public static void main(String[] args) {
File f = new File("/Users/Downloads");
for(File temp : f.listFiles()) {
if(temp.isFile()) {
System.out.println(temp.getName());
}
}
}
12345678
2)对于当前文件夹下的文件夹继续展开显示所有文件
private static void queryDirectory(File f, int level) {
if(f.isDirectory()) {
for(File temp : f.listFiles()) {
queryDirectory(temp, level + 1); //递归
}
}else {
for(int i = 0; i < level - 1; i++) {
System.out.print(" "); // 四个空格
}
System.out.println(f.getName()); //输出文件名
}
}
public static void main(String[] args) {
queryDirectory(new File("/Users/Downloads"),0);
}
12345678910111213141516
JDK1.7之后可以使用NIO.2的API实现
public static void main(String[] args) throws IOException {
Path initPath = Paths.get("/Users/Downloads");
Files.walkFileTree(initPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributesattrs) throws IOException {
System.out.println(file.getFileName().toString());
return FileVisitResult.CONTINUE;
}
});
}
12345678910
- TCP编程通过Socket套接字编程实现服务端向客户端发送信息
服务端:
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
//socket,TCP编程,服务端程序
public class Server {
public static void serverInfo(){
ServerSocket server = null;
Socket client = null;
PrintStream out = null;
try {
//在服务器8000端口等待客户连接
server = new ServerSocket(8000);
System.out.println("服务器正在等待客户端的连接......");
//程序阻塞,等待客户端的连接
client = server.accept();
System.out.println("连接客户端成功!!");
//实例化打印流对象,用于向客户端发送输出信息
out = new PrintStream(client.getOutputStream());
System.out.println("请输入您要向客户端发送的信息:");
Scanner scan = new Scanner(System.in); //获取输入流
//准备向客户端发送的信息
String info = "服务端向客户端发送信息:" + scan.nextLine();
//输出信息
out.println(info);
scan.close(); //关闭输入流
out.close(); //关闭输出打印流
client.close(); //关闭客户端
server.close(); //关闭服务器端的练级
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
serverInfo();//开启服务端
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
客户端:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
//socket编程,客户端,要与服务端的端口号一致
public class Client {
public static void clientInfo(){
//声明socket对象
Socket client = null;
try {
//实例化socket对象,指定连接的主机名称和端口号
client = new Socket("localhost",8000);
System.out.println("客户端连接成功");
//声明缓存字符流,用来接收信息
BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));
//读取信息
String info = buf.readLine();
//输出读取的信息
System.out.println("客户端收到服务器("+ client.getInetAddress() +")端发来的信息:【" + info + "】");
client.close();
buf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
clientInfo();
}
}
123456789101112131415161718192021222324252627282930313233343536
运行:
1、 开启服务端Server
Server:服务器正在等待客户端的连接…
2.、开启客户端Client
Server:(输出内容)
服务器正在等待客户端的连接…
连接客户端成功!!
请输入您要向客户端发送的信息:
Client:(输出内容)
客户端连接成功3、Server输入内容HelloWorld
客户端Client接收到信息:
客户端收到服务器(localhost/127.0.0.1)端发来的信息:【服务端向客户端发送信息:Hello World!!!】
- 饿汉式单例模式和懒汉式单例模式
1) 将构造器私有,不允许外界通过构造器
创建对象;
2) 通过公开的静态方法向外界返回类的唯一实例
饿汉式单例模式:
//饿汉式 不延迟实例化的做法,直接静态实例化创建 保证线程安全
public class HungarySingleton {
private static HungarySingleton instance=new HungarySingleton();
private HungarySingleton(){
}
public static HungarySingleton getInstance(){
return instance;//返回唯一实例
}
}
123456789
懒汉式单例模式:
public class LazySingleton {
private static LazySingleton instance = null;
private Singleton() {}
public static synchronized LazySingleton getInstance(){
if (instance == null) instance = new LazySingleton ();
return instance;
}
}
12345678
volatile关键字,DCL机制实现懒汉式,提高性能
//懒汉式 DCL机制
//T1先从主存拷贝数据到自己的线程内存,进行处理数据,处理完数据更新到主存
//T2从主存拷贝数据到自己的线程内存
public class LazySingleton {
private static volatile LazySingleton instance = null;
private LazySingleton(){
}
//unlock happen-before lock 语义
//T1,T2 ---可见性 T1 happen-before T2
/**
* A->B,B->C===A->C
* T2调用getInstance()和调用getName()
* T1调用getInstance()
* a++
* T1 ->T2
* 构造器内存操作->T1 ->T2
*/
public static LazySingleton getInstance(){
//T2
if(instance==null){
//T1 同步化 所有为null的只能进入一个,等待执行
synchronized(LazySingleton.class){
if(instance==null){
instance=new LazySingleton();
//JSR-133
//1.开辟空间
//2.初始化对象
//3.把地址给isntance变量---CPU
}
}
}
return instance;
}
}
1234567891011121314151617181920212223242526272829303132333435
- 能将int强制转换为byte类型的变量吗?如果该值
大于byte 类型的范围,将会出现什么现象?
可以做强制转换,但是Java中int 是32位(4byte)的,而byte 是8位(1byte)的,所以,如果强制转化是,int 类型的高24 位将会被丢弃,byte 类型的范围是从-128 到128。
- 哪个类包含clone 方法?是Cloneable 还是Object?
java.lang.Cloneable 是一个标示性接口,不包含任何方法, clone 方法在object 类中定义。并且clone() 方法是一个本地方法,这意味着它是由c 或c++ 或其他本地语言实现。
- Java 中++ (- -)操作符是线程安全的吗?
不是线程安全的操作。它涉及到多个指令,如读取变量值,增加/减少,然后存储回内存,这个过程可能会出现多个线程交差。
a = a + b
与a += b
的区别
+=
隐式的将加操作的结果类型强制转换为持有结果的类型(比如a += b
会将运算结果a+b
转换为需要a对应的类型)。如果两这个整型相加,如byte、short 或者int,首先会将它们提升到int 类型,然后在执行加法操作,最后再转换为接收的数据的类型。
而a = a + b
不会进行类型转换,如果越界就抛出异常
// byte取值 -128到128,超过就越界要转换类型
byte a = 120;
byte b = 120;
a = a + b; //编译报错:cannot convert from int to byte,因为不会将结果(a+b)自动强制类型转换
a += b; //编译正确,b = -16
//隐式的将加操作的结果类型强制转换为持有结果的类型(a+b的时候隐式转换为int相加
//结果强制转换为接收结果a的类型byte,a+b=240(int类型)转换为a的类型byte=-16)
1234567
- int 和Integer 哪个会占用更多的内存
Integer包装类会占用更多的内存。Integer 是一个对象,需要存储对象的元数据。但是int 是一个原始类型的数据,所以占用的空间更少
- “a==b”和”a.equals(b)”有什么区别?
如果a和b都是对象,则a==b 是比较两个对象的引用,只有当a和b指向的是堆中的同一个对象才会返回true,a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写equals() 方法, 所以可以用于两个不同对象,但是包含的字母相同的比较
- a.hashCode() 有什么用?与a.equals(b) 有什么关系?
hashCode()方法是相应对象整型的hash值。它常用于基于hash 的集合类,如Hashtable、HashMap、LinkedHashMap 等等。它与equals() 方法关系特别紧密。根据Java 规范,两个使用equal() 方法来判断相等的对象,必须具有相同的hash code。
- Java集合框架
poll()方法和remove()方法的区别
poll() 和remove() 都是从队列中取出一个元素,但是poll() 在获取元素失败的时候会返回空,但是remove() 失败的时候会抛出异常。
LinkedHashMap 和PriorityQueue 的区别
PriorityQueue保证最高或者最低优先级的的元素总是在队列头部,但是LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个PriorityQueue时,没有任何顺序保证,但是LinkedHashMap 课保证遍历顺序是元素插入的顺序。
ArrayList 与LinkedList 的主要区别
ArrrayList 底层的数据结构是数组,支持随机访问,而
LinkedList的底层数据结构是链表(双向链表),不支持随机访问(但是优化了更新操作)。使用下标访问一个元素,ArrayList 的时间复杂度是O(1),而LinkedList 是O(n)。Hashtable 与HashMap 有什么不同
1)Hashtable 是JDK 1 遗留下来的类,而HashMap 是后来增加的。
2)Hashtable 是同步的,比较慢,但HashMap 没有同步策略,所以会更快。
3)Hashtable不允许有个空的key,但是HashMap允许出现一个null key。Java中的HashSet,内部是如何工作
HashSet 的内部采用HashMap 来实现。由于Map 需要key 和value,所以所有key 的都有一个默认value。类似于HashMap, HashSet 不允许重复的key,只允许有一个null key,意思就是HashSet 中只允许存储一个null 对象。
ArrayList 和HashMap 的默认大小
ArrayList 的默认大小是10 个元素, HashMap的默认大小是16 个元素(必须是2的幂次)
两个相同的对象的hash code一定相同,两个对象有相同的hash Code,但两个对象不一定相同。
两个不相等的对象可能会有相同的hashcode 值, 这就是为什么在hashmap 中会有冲突。相等hashcode 值的规定只是说如果两个对象相等, 必须有相同的hashcode 值, 但是没有关于不相等对象的任何规定
Java中的TreeMap是使用红黑树实现的。