目录
1) Java 面向对象的三个特征与含义
封装:属性的封装和方法的封装。把属性定义为私有的,get(),set()方法。好处是信息隐藏和模块化,提高安全性。封装的主要作用在于对外隐藏内部实现细节,增强程序的安全性。
继承:子类可以继承父类的成员变量和成员方法。继承可以提高代码的用性。继承的特性:
a) 单一继承。
b) 子类只能继承父类的非私有成员变量和方法。
c) 成员变量的隐藏和方法的覆盖。
多态:当同一个操作作用在不同对象时,会产生不同的结果。
2) 面向对象的6 个基本原则
单一职责:是指一个类的功能要单一,一个类只负责一个职责。一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则。
开放封闭:软件实体应当对扩展开放,对修改关闭。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。在开发阶段,我们都知道,如果对一个功能进行扩展,如果只是一味地对方法进行修改,可能会造成一些问题,诸如可能会引入新的bug,或者增加代码的复杂度,对代码结构造成破坏、冗余,还需要重新进行全面的测试。那么该怎么解决这些问题?很简单,这就需要系统能够支持扩展,只有扩展性良好的系统,才能在不进行修改已有实现代码的基础上,引进新的功能。
里氏替换:任何使用基类的地方,都能够使用子类替换,而且在替换子类后,系统能够正常工作。子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。一个软件实体如果使用的是一个基类,那么当把这个基类替换成继承该基类的子类,程序的行为不会发生任何变化。软件实体察觉不出基类对象和子类对象的区别。
接口隔离: 即应该将接口粒度最小化,将功能划分到每一个不能再分的子角色,为每一个子角色创建接口,通过这样,才不会让接口的实现类实现一些不必要的功能。建立单一的接口,不要建立臃肿的庞大的接口,也就是说接口的方法尽量少。接口要小而专,绝不能大而全。臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。
依赖倒置:即我们的类要依赖于抽象,而不是依赖于具体,也就是我们经常听到的“要面向接口编程”。(该原则说得具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代) 。依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。减少类间的耦合性。
合成/聚合复用:优先使用聚合或合成关系复用代码。
3) java 多态的实现原理
有2 种方式来实现多态,一种是编译时多态,另外一种是运行时多态;编译时多态是通过方法的重载来实现的,运行时多态是通过方法的重写来实现的。
方法的重载,指的是同一个类中有多个同名的方法,但这些方法有着不同的参数。在编译时就可以确定到底调用哪个方法。
方法的重写,子类重写父类中的方法。父类的引用变量不仅可以指向父类的实例对象,还可以指向子类的实例对象。当父类的引用指向子类的对象时,只有在运行时才能确定调用哪个方法。
特别注意:只有类中的方法才有多态的概念,类中成员变量没有多态的概念。
4) 继承和组合区别
组合和继承是代码复用的2 种方式。
组合是在新类里面创建原有类的对象,重复利用已有类的功能。
组合关系在运行期决定,而继承关系在编译期就已经决定了。
使用继承关系时,可以实现类型的回溯,即用父类变量引用子类对象,这样便可以实现多态,而组合没有这个特性。
从逻辑上看,组合最主要地体现的是一种整体和部分的思想,例如在电脑类是由内存类,CPU 类,硬盘类等等组成的,而继承则体现的是一种可以回溯的父子关系,子类也是父类的一个对象。
5) Override(覆盖、重写)和Overload(重载)的区别
重载和覆盖是java 多态性的不同表现方式。
重载是在一个类中多态性的一种表现,是指在一个类中定义了多个同名的方法,但是他们有不同的参数个数或有不同的参数类型。在使用重载时要注意:重载只能通过不同的方法参数来区分。例如不同的参数类型,不同的参数个数,不同的参数顺序。不能通过访问权限、返回类型、抛出的异常进行重载。
覆盖是指子类函数覆盖父类中的函数。在覆盖时要注意以下几点:
a) 覆盖的方法的函数名和参数必须要和被覆盖的方法的函数名和参数完全匹配,才能达到覆盖的效果;覆盖的方法的返回值必须和被覆盖的方法的返回值类型一致;
b) 覆盖的方法所抛出的异常必须和被覆盖方法的所抛出的异常一致,或者是其子类;
c) 被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
d) 子类函数的访问修饰权限要大于等于父类的(public > protected > default > private)
特别注意:Java 中,子类无法覆盖父类的static 方法或private 方法。
6) 接口与抽象类的区别
语法层面上的区别
a) 抽象类可以提供成员方法的实现细节(注:可以只包含非抽象方法),而接口中只能存在public abstract 方法,方法默认是public abstract 的,但是,java8 中接口可以有default 方法;
b) 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final 类型的;
c) 抽象类可以有静态代码块和静态方法和构造方法;接口中不能含有静态代码块以及静态方法以及构造方法。但是,java8 中接口可以有静态方法;
d) 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
设计层面上的区别
a) 抽象层次不同。抽象类是对类的整体抽象,包括属性和行为的抽象。而接口只是对行为的抽象。
b) 跨域不同。抽象类所体现的是一种继承关系,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已,其设计理念是“has-a”的关系(有没有、具备不具备的关系),实现它的子类可以不存在任何关系,共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly 接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!
c) 设计层次不同。对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这时就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们再抽象他们的共同点形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
7) 静态变量与非静态变量的区别?
内存分配: 静态变量在应用程序初始化时,就存在于内存当中,直到它所在的类的程序运行结束或者被卸载时才消亡;而非静态变量需要被实例化后才会分配内存。
生存周期:静态变量生存周期为应用程序的存在周期;非静态变量的存在周期取决于实例化的类的存在周期。
调用方式:静态变量一般通过“类.静态变量名”调用;非静态变量当该变量所在的类被实例化后,可通过实例化的类名直接访问。
共享方式:静态变量是全局变量,被所有类的实例对象共享,即一个实例的改变了静态变量的值,其他同类的实例读到的就是变化后的值;非静态变量是局部变量,不共享的。
访问方式:静态成员不能访问非静态成员;非静态成员可以访问静态成员。静态变量在类装载的时候分配内存,以后创建的对象都使用的该内存,相应的操作也就是对这块内存进行操作。也可以看作是另类的全局变量。
8) 静态内部类和非静态内部类的区别?
静态内部类不依赖于外部类实例而被实例化,而非静态内部类需要在外部类实例化后才可以被实例化。
静态内部类不需要持有外部类的引用。但非静态内部类需要持有对外部类的引用。
静态内部类不能访问外部类的非静态成员和非静态方法。它只能访问外部类的静态成员和静态方法。非静态内部类能够访问外部类的静态和非静态成员和方法。
9) 内部类都有哪些?
有四种:静态内部类,非静态内部类,局部内部类(在外部类的方法中定义的类,它只能访问方法中定义为final类型的局部变量。),匿名内部类。
匿名内部类:
匿名内部类一定是在new 的后面,这个匿名内部类必须继承一个父类或者实现一个接口。
匿名内部类不能有构造函数。
只能创建匿名内部类的一个实例。
在Java 8 之前,如果匿名内部类需要访问外部类的局部变量,则必须使用final 来修饰外部类的局部变量。在现在的Java 8 已经去取消了这个限制。
10) 什么是泛型,为什么要使用以及类型擦除?
泛型的本质就是“参数化类型”,也就是说所操作的数据类型被指定为一个参数。创建集合时就指定集合元素的数据类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。
Java 编译器生成的字节码是不包含泛型信息的,泛型类型信息将在编译处理时被擦除,这个过程即类型擦除。类型擦除可以简单的理解为将泛型java 代码转换为普通java 代码,只不过编译器更直接点,将泛型java 代码直接转换成普通java 字节码。类型擦除的主要过程如下:
一.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
二.移除所有的类型参数。
11) 泛型的好处
在编译的时候检查类型安全,确保只能把正确类型的对象放入集合中;消除强制类型转换。
12) Iterator迭代器与Iterable的关系
Iterator内部的 next()方法和 hasNext()方法都依赖于当前迭代的位置
一次迭代后,“指向”或者说记录当前迭代位置的“指针”,会指向该集合的末尾,如果Collection实现的是Iterator的话,那么下一次再迭代的时候就需要另外写一个方法,将指针“移动到”集合的首部,以便重新遍历。
如果Collection实现的是Iterator的话,Collection内部也是只有一个迭代器,那么在多线程环境下A的迭代会影响B的迭代(多线程或者其他情况)。
如果实现的是Iterable,然后通过iterator方法来返回一个新的迭代器对象,迭代器之间互不干扰。
13) CAS:
多步操作:例如getAndSet(intnewValue)是两步操作–>先获取值,再设置值,所以需要原子化,这里采用CAS实现。
对于方法是返回旧值还是新值,直接看方法是以get开头(返回旧值)还是get结尾(返回新值)就好。
CAS:比较CPU内存上的值是不是当前值current,如果是就换成新值update,如果不是,说明获取值之后到设置值之前,该值已经被别人先一步设置过了,此时如果自己再设置值的话,需要在别人修改后的值的基础上去操作,否则就会覆盖别人的修改,所以这个时候会直接返回false,再进行无限循环,重新获取当前值,然后再基于CAS进行加减操作。
1) Volatile
保证了不同线程对这个变量进行读取时的可见性。即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(volatile 解决了线程间共享变量的可见性问题)。
禁止进行指令重排序,阻止编译器对代码的优化。
2) strictfp
strictfp 可以用来修饰一个类、接口或者方法,在所声明的范围内,所有浮点数的计算都是精确的。当一个类被strictfp 修饰时,所有方法默认也被strictfp修饰。
3) transient
它修饰的变量,在序列化时其变量值是不被保存的。
4) static使用方法
修饰类(静态内部类),修饰成员变量(静态变量),修饰成员方法(静态成员方法),静态代码块
5) finalize 的用法
它是Object 类的一个方法,在垃圾回收器执行时会调用被回收对象的finalize()方法。
6) finally 的用法:
finally 是异常处理的一部分,只能用在try-catch 语句中,表示这段代码一般情况下,一定会执行。经常用在需要释放资源的情况下。
7) final 的用法:
final 可以用来修饰类,变量和方法。
当一个类被final 修饰的时候,表示该类不能被继承。类中方法默认被final 修饰。
当final 修饰基本数据类型的变量时,表示该值在被初始化后不能更改;
当final 修饰引用类型的变量时,表示该引用在初始化之后不能再指向其他的对象。注意:final 修饰的变量必须被初始化。可以在定义的时候初始化,也可以在构造函数中进行初始化。
当final 修饰方法时,表示这个方法不能被子类重写。
使用final 方法的原因有2 个:
第一、把方法锁定,防止任何继承类修改它的实现。
第二、高效。当要使用一个被声明为final 的方法时,直接将方法主体插入到调用处,而不进行方法调用,可以提高程序的执行效率(ps.如果过多的话,这样会造成代码膨胀)。
可以通过Collections.unmodifiableXXX:Collection、List、Set、Map或者Guava:ImmutableXXX:Collection、List、Set、Map。将定义为不可变对象。
1) new String(“abc”);创建了几个对象?
1 个或者2 个对象。如果常量池中原来有”abc”,那么只创建一个对象;如果常量池中原来没有字符串”abc”,那么就会创建2 个对象。
2) 修改String 对象的原理
因为string 对象是不可变的,每次对String 类型进行改变的时候,都会生成一个新的String 对象。首先创建一个StringBuffer 对象,然后调用append()方法,最后调用toString()方法。String s=”Hello”; S+=“”World”;等价于:StringBuffer sb=new StringBuffer(s); sb.append(“World”); sb.toString();
3) 什么是不可变类?
不可变类:当创建了一个类的实例后,就不允许修改它的值了。特别注意:String 和包装类(Integer,Float…)都是不可变类。String 采用了享元设计模式(flyweight)
4) 为什么String要设计成不可变的?
需要综合内存数据结构以及安全等方面考虑这个问题。
a) 字符串常量池的需要
b) 允许String对象缓存HashCode。Java中String对象的哈希码被频繁地使用,缓存后效率提高。
c) 安全性。String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
d) 为了防止扩展类无意间破坏原来方法的实现。
总体来说, String不可变的原因包括:设计考虑,效率优化问题,以及安全性这三大方面。
5) java 为什么需要常量池?
jvm 使用常量池来保存跟踪当前类中引用的其他类及其成员变量和成员方法。
避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享
节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间
节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等
6) String、StringBuffer 与StringBuilder 的区别?
string 对象是不可变的;StringBuilder 与StringBuffer 对象是可变的。
String 和StringBuffer 是线程安全。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
初始化方式的不同,StringBuffer 和StringBuilder 只能用构造函数的形式来初始化。String 除了用构造函数进行初始化外,还可以直接赋值。
7) StringBuilder 和StringBuffer 底层是怎么实现的。
每个StringBuffer 对象都有一定的缓冲区容量(可变长的字符数组,类似于ArrayList 的底层实现),默认初始容量为16个字符。当字符串大小没有超过容量时,不会分配新的容量;当字符串大小超过容量时,会自动扩容(扩容后的容量大约是之前的2倍)。StringBuilder 和StringBuffer,字符串都是存放在char[]中的。
1) 占据几个字节?
占1个字节:byte,boolean
占2个字节:char,short(-128 — 127 缓存)
占4个字节:int(-128 — 127 缓存),float(没有缓存)
占8个字节:long(-128 — 127 缓存),double(没有缓存)
缓存指的是包装类的缓存。用valueOf(i)取值会先得到缓存中的值。
2) 基本数据类型和对应的包装类的区别?
初始值的不同。包装类的对象默认初始值是null,基本数据类型变量的默认初始值根据变量类型不同而不同,如int 的默认初始值是0。
存储方式及位置不同,基本类型是直接将变量值存储在堆栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
声明方式不同,基本类型不适用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
使用方式不同,基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。
3) 数据类型自动转换优先级?
低---------------------------------------------> 高
byte,short,char-> int -> long -> float -> double
4) 运算符的优先级?
(++,–)> (*,/,%) > (+,-) > (<<,>>) > (&) > ( | ) >&& > ||
5) 强制类型转换时的规则有哪些
当对小于int 的数据类型(byte,char,short)进行运算时,首先会把这些类型的变量值强制转为int,类型对int 类型的值进行运算,最后得到的值也是int 类型的。因此,如果把2个short 类型的值相加,最后得到的结果是int 类型,如果需要得到short 类型的结果,就必须显示地运算结果转为short 类型。
基本数据类型和boolean 类型是不能相互转换的。
char 类型的数据转为高级类型时,会转换为对应的ASCII 码。
1) 定义:
反射机制是在运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法。在java 中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。
2) 反射机制主要提供的功能:
在运行时判定任意一个对象所属的类;
在运行时创建对象;
在运行时判定任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理。
3) 哪里用到反射机制?
jdbc 中有一行代码Class.forName(‘com.mysql.jdbc.Driver.class’);//加载MySql 的驱动类。这就是反射,现在很多框架都用到反射机制,hibernate ,struts 都是用反射机制实现的。
4) 反射的实现方式
在Java 中实现反射最重要的一步,也是第一步就是获取Class 对象,得到Class 对象后可以通过该对象调用相应的方法来获取该类中的属性、方法以及调用该类中的方法。有4 种方法可以得到Class 对象:
1) NIO原理?NIO 主要用来解决什么问题?
在NIO 中有几个核心对象:缓冲区(Buffer)、通道(Channel)、选择器(Selector)。
缓冲区(Buffer):缓冲区实际上是一个容器对象,其实就是一个数组,在NIO 库中,所有据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,它也是写入到缓冲区中的;任何时候访问NIO 中的数据,都是将它放到缓冲区中。在NIO 中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer。而在面向流I/O 系统中,所有数据都是直接写入或者直接将数据读取到Stream 对象中。
通道(Channel):通道是一个对象,通过它可以读取和写入数据,所有数据都通过Buffer 对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是InputStream 或者OutputStream 的子类,比如InputStream 只能进行读取操作,OutputStream只能进行写操作),而通道是双向的,可以用于读、写或者同时用于读写。当有读或写等任何注册的事件发生时,可以从Selector中获得相应的SelectionKey,同时从SelectionKey 中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获得客户端发送过来的数据。
选择器(Selector):NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel 告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel 中读取数据,放心,包准能够读到,接着我们可以处理这些数据。Selector 内部原理实际是在做一个对所注册的channel 的轮询访问,不断地轮询,一旦轮询到一个channel 有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel 的内容。Selector 的作用就是用来轮询每个注册的Channel,一旦发现Channel 有注册的事件发生,便获取事件然后进行处理。用单线程处理一个Selector,然后通过Selector.select()方法来获取到达事件,在获取了到达事件之后,就可以逐个地对这些事件进行响应处理。Selector 类是NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
2) Java 中的NIO,BIO 分别是什么?
Java NIO 和IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。在Java NIO中把数据读取到一个缓冲区中,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞IO。Java IO 的各种流是阻塞的。这意味着,当一个线程调用read() 或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO 的空闲时间用于在其它通道上执行IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
选择器。Java NIO 的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。为了将Channel 和selector 配合使用,必须将channel 注册到selector 上,通过SelectableChannel.register()方法来实现。这种选择机制,使得一个单独的线程很容易来管理多个通道。只要Channel 向Selector 注册了某种特定的事件,Selector 就会监听这些事件是否会发生,一旦发生某个事件,便会通知对应的Channel。使用选择器,借助单一线程,就可对数量庞大的活动I/O 通道实施监控和维护。
1) 单例模式: java.lang.Runtime 。
Runtime 类封装了Java 运行时的环境。每一个java 程序实际上都是启动了一个JVM 进程,那么每个JVM 进程都是对应这一个Runtime 实例,此实例是由JVM 为其实例化的。每个Java 应用程序都有一个Runtime 类实例,使应用程序能够与其运行的环境相连接。由于Java 是单进程的,所以,在一个JVM中,Runtime 的实例应该只有一个。所以应该使用单例来实现。一般不能实例化一个Runtime 对象,应用程序也不能创建自己的Runtime 类实例,但可以通过getRuntime 方法获取当前Runtime 运行时对象的引用。
2) 享元模式:
String 常量池和Integer 等包装类的缓存策略:Integer.valueOf(int i)等。
3) 原型模式:
Object.clone;Cloneable。
4) 装饰器模式:IO 流中。
5) 迭代器模式:Iterator 。