解释下什么是面向对象?
答:面向对象(OOP)是一种基于面向过程的新编程思想,就是站在对象的位置去思考问题,将数据以及对数据的操作封装到一个对象中。面向对象是一种符合人类思维习惯的编程思想,在程序中使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系。
面向对象与面向过程的区别?
答:面向过程是一种较早的编程思想该思想是站在过程的角度思考问题,强调的是功能的行为,功能的执行过程,也就是说执行的先后顺序。就是分析出解决问题所需要的步骤,然后用函数将这些步骤一一实现,按相应的顺序进行调用。
优点:性能比面向对象高,因为类调用是需要实例化,开销较大,比较消耗资源。
缺点:没有面向对象易维护、易复用、易扩展。
面向对象是一种基于面向过程的新的编程思想,就是站在对象的位置思考问题,强调的是封装了数据以及对数据的操作的对象。则是把构成问题的事务按照一定规则分为多个独立的对象,然后通过调用对象的方法来解决。
优点:易维护、易复用、易扩展,由于封装继承多态的特性,可以设计出低耦合的系统。
缺点:性能相较面向过程低。
例如:将大象放进冰箱这个过程。
面向过程的做法就是,第一步打开冰箱,第二步将大象塞进冰箱,第三步关闭冰箱门。
面向对象的做法就是,将这个功能交给一个对象进行实现,只需要调用一个对象,让对象进行三步操作,将大象装进冰箱。
面向对象的三大特征?分别解释一下
答:封装、继承和多态
封装(核心思想):将对象的属性和方法封装起来,不需要让外界知道具体实现细节。
继承:主要描述的是类与类之间的关系,通过继承,可以在无需重新编写原有类的情况下,对原有类的功能进行扩展。
多态:指的是在程序中允许出现重名现象,他指在一个类中定义的属性和方法被其他类继承后,它们可以具有不同的数据类型或表现不同的行为,这使得同一属性和方法在不同的类中具有不同的语义。
JDK、JRE、JVM三者之间的关系
答:JDK是Java开发工具包提供了Java的开发环境和运行环境等,JRE是Java运行环境为Java的运行提供了所需的环境和Java程序所需要的核心库等,JVM是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。Java开发工具包包括JRE和JVM,所以在配置Java环境时,只需要下载安装JDK。JRE包含Java虚拟机和Java程序所需的核心类库等。
什么是跨平台?原理是什么
就是说Java语言,一次编译,到处运行,可以运行到多个系统平台上。
原理:Java程序是通过Java虚拟机在系统平台上运行的,只要该系统可以安装相应的Java虚拟机,该系统就可以运行Java程序。
Java语言有哪些特点
简单易学、面向对象、平台无关性、支持网络编程并且很方便、支持多线程、健壮性、安全性。
Java中的修饰符
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
什么是字节码?采用字节码的最大好处是什么?
字节码就是Java程序经过虚拟机编译器编译后产生的.class文件,他不面向任何特定的处理机,只面向虚拟机。
好处:Java通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植性的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无需重新编译便可在不同的计算机上运行。
java中的编译器和解释器
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。
Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中 解释器-----> 机器可执行的二进制机器码---->程序运行。
Java和C++的区别
-
都是面向对象的语言,都支持封装、继承和多态
-
Java不提供指针来直接访问内存,程序内存更加安全
-
Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是接口可以多继承。
-
Java有自动内存管理机制,不需要程序员手动释放无用内存
重载(Overload)和重写(Override)的区别
重载:位于一个类或其子类中,方法名相同,但参数列表不同,返回值可以相同也可以不同。就是对于不同的情况调用不同的方法。
-
方法名相同
-
参数列表一定不同
-
访问修饰符和返回值类型可以相同也可以不同
重写:是指子类对父类的方法进行重写,方法名相同,参数列表相同,返回值相同,但方法体有差别。
-
方法名相同、参数列表相同、返回值相同、具体实现有差别
-
访问权限不能比父类中被重写的方法权限更低
-
构造方法不能被重写
-
被private或final或static修饰的方法不能被重写
就是具体的实现类对于父类的该方法实现不满意,需要自己在写一个满足于自己要求的方法。
用最有效率的方法计算2乘以8?
2>>3
Java语言采用何种编码方案?有何特点?
Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一 个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。
数组有没有length()方法?String有没有length()方法?
数组没有length()方法,有length属性,数组array.length返回的是该数组的长度。字符串String是有length()方法的,str.length()返回的是该字符串的长度。
构造器(constructor)是否可被重写(override)?
重写是发生在子类与父类中,方法名、参数列表、返回值、访问修饰符和异常都相同。首先,构造器不能被继承,因为每个类名都不相同,而构造器的名称与类名相同,这肯定不能算是继承,所以,既然构造器不能被继承,因此不能被重写,但可以被重载。
是否可以继承String类?
不能继承String类,因为String类是被final修饰的
java 中操作字符串都有哪些类?它们之间有什么区别?
操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。
什么情况下用+运算符进行字符串连接比调用StringBuffer/StringBuilder对象的append方法连接字符串性能更好?
+的拼接方式实际上是通过new 一个StringBuilder来进行拼接,如果只是简单的拼接,两者差不多,+的可读性更高,但如果多次拼接的话,建议StringBuilder,因为每次用+号拼接都会new一个新的StringBuilder进行操作,浪费资源,影响性能。
Java中是否可以重写一个private或者static方法?
不能,因为方法的重写是基于运行时动态绑定的,而static修饰的方法是编译时静态绑定的。而private修饰的成员是外界看不到的,所以不存在重写的问题。
String是最基本的数据类型么
不是,基础类型有八种:
基本类型 | 字节数 |
---|---|
byte | 1字节 |
short | 2字节 |
int | 4字节 |
long | 8字节 |
float | 4字节 |
double | 8字节 |
char | 2字节 |
boolean | 两个取值true、false |
构造方法有哪些特征?
-
完成对象的初始化。
-
把定义对象是的参数传给对象的域
-
名称与类名相同
-
没有返回值
-
一个类可以有多个构造函数
在Java中定义一个不做事且没有参数的构造方法有什么作用?
Java 程序在执行子类的构造方法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类 中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。
什么是数据结构?
计算机保存、组织数据的方式
Java的数据结构有哪些?
-
线性表(ArrayList)
-
链表(LinkedList)
-
栈(Stack)
-
队列(Queue)
-
图(Map)
-
树(Tree)
Java中创建对象的几种方式?
-
new创建新对象
-
通过反射机制创建对象
-
通过clone机制
-
通过序列化机制
反射中,Class.forName和classloader的区别
java中class.forName()和classLoader都可用来对类进行加载。
class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。
抽象类和接口有什么区别?
不同点:
接口只有方法的定义,没有方法的具体实现,实现接口的类要实现接口的所有方法;抽象类可以有定义和实现
接口与类是实现关系,并且可以实现多个接口;抽象类与类是继承关系,只能继承一个抽象类
接口中的成员方法全都是public,抽象类中可以有抽象方法,也可以没有,抽象方法前要加abstract
接口中不能出现静态方法,抽象类中可以
接口不能定义构造器,抽象类可以
相同点:
都不能通过new来实例化,只能通过实现类实例化
可以将抽象类和接口类作为引用类型
一个类如果实现接口或继承抽象类,必须实现全部抽象方法,否则仍然是个抽象类
静态变量和实例变量的区别?
静态变量也叫做类变量,有static修饰,而实例变量没有static修饰
静态变量不属于某个实例对象,而是属于整个类。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就回被分配空间,静态变量就可以被使用了。
实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。
short s1 = 1;s1 = s1 +1;有什么错?那么改为 s1 += 1;呢,有没有错误,为什么?
第一个有错,第二个没错,因为+=运算符会默认将最后的结果进行强转。
Integer和int的区别?
int是Java的基本类型,而Integer是int的包装类型,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
class AutoUnboxingTest { public static void main(String[] args) { Integer a = new Integer(3); Integer b = 3; // 将3自动装箱成Integer类型 int c = 3; System.out.println(a == b); // false 两个引用没有引用同一对象 System.out.println(a == c); // true a自动拆箱成int类型再和c比较 } }
public class Test03 { public static void main(String[] args) { Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150; System.out.println(f1 == f2);//true System.out.println(f3 == f4);//false } }
如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象。
装箱和拆箱的区别?
装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
Integer a= 127与Integer b = 127相等吗
对于对象引用类型:==比较的是对象的内存地址。
对于基本数据类型:==比较的是值。如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer 对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false。
switch语句能否作用在byte上,能否作用在long上,能否作用在String上?
在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,Java中引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
Java有没有goto
goto是Java中的保留字,在目前版本的Java中没有使用。
final有什么用?
用于修饰类、属性和方法;
-
被final修饰的类不可以被继承
-
被final修饰的方法不可以被重写
-
被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
final、finally、finalize的区别
-
final:修饰符(关键字)有三种用法:如果一个类被声明为final,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。将变量声明为final,可以保证它们在使用中不被改变,被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取不可修改。被声明为final的方法也同样只能使用,不能在子类中被重写。
-
finally:通常放在try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。
-
finalize:Object类中定义的方法,Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他清理工作。
this关键字的用法
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this的用法在java中大体可以分为3种:
-
普通的直接引用,this相当于是指向当前对象本身。
-
形参与成员名字重名,用this来区分。
-
引用本类的构造函数
super关键字的用法
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super也有三种用法:
-
普通的直接引用
与this类似,super相当于是指向当前对象的父类的引用,这样就可以用
super.xxx来引用父类的成员。
-
用super来区分子类与父类中的同名方法或者变量。
-
super(参数)调用父类中的某一个构造方法。
this与super的区别
-
super引用当前对象的直接父类中的成员;this代表当前对象。
-
super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
-
super()和this()均需放在构造方法内第一行。
-
this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
-
this()和super()都指的是对象,所以,均不可以在static环境中使用。
-
从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。、
static关键字
static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static的特点:
-
被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
-
在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
-
static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
-
被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
static应用场景:
-
修饰成员变量
-
修饰成员方法
-
静态代码块
-
修饰类【只能修饰内部类也就是静态内部类】
-
静态导包
break,continue,return的区别及作用
break 跳出总上一层循环,不再执行循环(结束当前的循环体)
continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
在Java中,如何跳出当前的多重嵌套循环?
-
使用break+标记。例:
public static void main(String[] args) { System.out.println("标记前"); ok: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.println("i=" + i + ",j=" + j); if (j == 5) break ok; } } System.out.println("标记后"); }
-
使外层的循环条件表达式的结果可以受到里层循环体代码的控制
public static void main(String[] args) { System.out.println("标记前"); boolean flag = true; for (int i = 0; i < 10; i++) { for (int j = 0; j < 10 && flag; j++) { System.out.println("i=" + i + ",j=" + j); if (j == 5) flag = false; } } System.out.println("标记后"); }
什么是多态机制?Java语言是如何实现多态的?
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 多态分为编译时多态和运行时多态。编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数。运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
面向对象五大基本原则是什么
-
单一职责原则SRP(Single Responsibility Principle)类的功能要单一,不能包罗万象,跟杂货铺似的。
-
开放封闭原则OCP(Open-Close Principle)一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
-
里式替换原则LSP(the Liskov Substitution Principle LSP)子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。
-
依赖倒置原则DIP(the Dependency Inversion Principle DIP)高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。
-
接口分离原则ISP(the Interface Segregation Principle ISP)设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。
对象实例与对象引用有何不同?
建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根 绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以 用n条绳子系住一个气球)
匿名内部类
-
匿名内部类就是没有名字的内部类
-
匿名内部类必须继承一个抽象类或者实现一个接口。
-
匿名内部类不能定义任何静态成员和静态方法。
-
当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
-
匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须 要加上final?
定义在类内部的静态类,就是静态内部类。
定义在类内部,成员位置上的非静态类,就是成员内部类。
定义在方法中的内部类,就是局部内部类。
匿名内部类就是没有名字的内部类。
public class Outer { void outMethod(){ final int a =10; class Inner { void innerMethod(){ System.out.println(a); } } } }
是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变 量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final, 可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。
==和equals的区别
==:
对于基本类型,比较的是值是否相同
对于引用类型,比较的是引用是否相同
equals:
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。
== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用;而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
两个对象的hashCode()相同,则equals()也一定是true吗?
不一定,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
如果两个对象相等,则hashcode一定也是相同的两个对象相等,对两个对象分别调用equals方法都返回true 两个对象有相同的hashcode值,它们也不一定是相等的。
equals方法被覆盖过,则hashCode方法也必须被覆盖
hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指 向相同的数据)
&和&&的区别?
相同点:&和&&都可以用作逻辑与的运算符,表示逻辑与(and)
不同点:
-
&&是短路逻辑与,当前面的条件能够判断真个表达式的值,则不会再看后面的表达式,而&没有短路功能
-
&作为逻辑与,只有两边全为true时,结果才为true,而&&运算符,当地一个表达式为false时,结果为false,不会计算后一个表达式。
-
&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如:0x31 & 0x0f的结果为0x01。
Java中的参数传递时值传递,还是传引用?
Java都是值传递
值传递:对象被值传递,意味着传递了一个该对象的副本,改变副本,也不会影响源对象的值。
引用传递:对象被引用传递,意味着传递的并不是实际的对象,而是对象的引用,因此,外部对引用对象所做的改变,都会映射到源对象上。
-
一个方法不能改变一个基本类型
-
一个方法可以改变一个对象参数的状态(简单理解可以改变所传参数的属性值)
-
一个方法不能改变对象参数的引用,也就是说不能让对象参数指向一个新的对象
Java中的Math.round(-1.5)等于多少?
-1,因为round函数会将其进行四舍五入的操作,四舍五入的原理是在参数上加0.5然后进行下取整。
如何实现对象的克隆?
-
实现Cloneable接口并重写Object类中的clone()方法;
-
实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。
深克隆和浅克隆的区别
浅克隆是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
深克隆不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class MyUtil { private MyUtil() { throw new AssertionError(); } @SuppressWarnings("unchecked") public static <T extends Serializable> T clone(T obj) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bout); oos.writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bin); return (T) ois.readObject(); // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义 // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放 } } /** * 人类 * */ class Person implements Serializable { private static final long serialVersionUID = -9102017020286042305L; private String name; // 姓名 private int age; // 年龄 private Car car; // 座驾 public Person(String name, int age, Car car) { this.name = name; this.age = age; this.car = car; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Car getCar() { return car; } public void setCar(Car car) { this.car = car; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + ", car=" + car + "]"; } } /** * 小汽车类 * */ class Car implements Serializable { private static final long serialVersionUID = -5713945027627603702L; private String brand; // 品牌 private int maxSpeed; // 最高时速 public Car(String brand, int maxSpeed) { this.brand = brand; this.maxSpeed = maxSpeed; } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public int getMaxSpeed() { return maxSpeed; } public void setMaxSpeed(int maxSpeed) { this.maxSpeed = maxSpeed; } @Override public String toString() { return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]"; } } class CloneTest { public static void main(String[] args) { try { Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300)); Person p2 = MyUtil.clone(p1); // 深度克隆 p2.getCar().setBrand("BYD"); // 修改克隆的Person对象p2关联的汽车对象的品牌属性 // 原来的Person对象p1关联的汽车不会受到任何影响 // 因为在克隆Person对象时其关联的汽车对象也被克隆了 System.out.println(p1); } catch (Exception e) { e.printStackTrace(); } } }
什么是Java的序列化?如何实现Java的序列化?
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
序列化:将 Java 对象转换成字节流的过程。
反序列化:将字节流转换成Java对象的过程。
序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。
Serializable接口 :
优点:内建支持,易于实现。
缺点:占用空间但,由于额外的开销导致速度慢
Externalizable接口 :
优点:开销较少,可能的速度提升
缺点:虚拟机不提供任何帮助,也就是说所有的工作都落到了开发人员的肩上。
注意事项
-
某个类可以被序列化,则其子类也可以被序列化
-
声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据
-
反序列化读取序列化对象的顺序要保持一致
实现序列化:需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆。
package constxiong.interview; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 测试序列化,反序列化 * @author ConstXiong * @date 2019-06-17 09:31:22 */ public class TestSerializable implements Serializable { private static final long serialVersionUID = 5887391604554532906L; private int id; private String name; public TestSerializable(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "TestSerializable [id=" + id + ", name=" + name + "]"; } @SuppressWarnings("resource") public static void main(String[] args) throws IOException, ClassNotFoundException { //序列化 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TestSerializable.obj")); oos.writeObject("测试序列化"); oos.writeObject(618); TestSerializable test = new TestSerializable(1, "ConstXiong"); oos.writeObject(test); //反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TestSerializable.obj")); System.out.println((String)ois.readObject()); System.out.println((Integer)ois.readObject()); System.out.println((TestSerializable)ois.readObject()); } } 结果: 测试序列化 618 TestSerializable [id=1, name=ConstXiong]
什么情况下需要序列化?
序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆。
-
对象序列化可以实现分布式对象。主要应用例如:RMI要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。
-
java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。
Java的泛型是如何工作的?什么是类型擦除?
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型通过类型擦除来将变量变为一个类型,编译器在编译期间擦出了所有类型的相关信息,所以在运行时不存在任何类型的相关信息。例如:List <String>
在运行时仅以 List 来表示;
-
这样做可以确保能和 Java 5 之前的版本开发二进制类库兼容;
-
无法在运行时访问到数据类型,因为编译器已经把泛型类型转换成了原始类型;
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
如在代码中定义List<Object>
和List<String>
等类型,在编译后都会变成List,JVM看到的只是List
,而由泛型附加的类型信息对JVM是看不到的。
什么是泛型中的限定通配符和非限定通配符
常用的 T,E,K,V,?
-
? 表示不确定的 java 类型,无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型
-
T (type) 表示具体的一个java类型
-
K V (key value) 分别代表java键值中的Key Value
-
E (element) 代表Element
有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。
限定通配符:
上界通配符 < ? extends E>,上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
下界通配符 < ? super E>,下界:用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
非限定通配符:
可以用任意类型来替代,类型为<T>
ArrayList和LinkedList的区别
都实现了List接口。
ArrayList是基于索引的数据接口,他的底层是数组,对查找操作简易。
LinkedList是以元素列表的形式存储数据,底层是双向链表。
ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。
LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
LinkedList比ArrayList更占内存,因为一个结点需要存储两个引用,分别指向前后。
但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
Collection和Collections的区别?
Collection是一个接口,它是Set、List等容器的父接口;Collections是个一个工具类,提供了一系列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。
Java中反射是什么意思?有哪些应用场景?
原理:java虚拟机在加载完类之后,在堆内存的方法区中就产生了一个Class
类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子
, 透过这个镜子看到类的结构,所以,我们形象的称之为:反射
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
-
我们在使用 JDBC 连接数据库时使用
Class.forName()
通过反射加载数据库的驱动程序; -
Spring 框架的 IOC(动态加载管理 Bean)创建对象以及 AOP(动态代理)功能都和反射有联系;
-
动态配置实例的属性
静态编译:在编译时确定类型,绑定对象
动态编译:运行时确定类型,绑定对象
反射的优缺点
优点:能够运行时动态获取类的实例,提高灵活性;与动态编译结合。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
1,性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。2,安全问题,让我们可以动态操作改变类的属性同时也增加了类的安全隐患。
Java获取反射的三种方法
-
通过new对象实现反射
-
通过路径实现反射
-
通过类名实现反射
public class Student { private int id; String name; protected boolean sex; public float score; } public class Get { //获取反射机制三种方式 public static void main(String[] args) throws ClassNotFoundException { //方式一(通过建立对象) Student stu = new Student(); Class classobj1 = stu.getClass(); System.out.println(classobj1.getName()); //方式二(所在通过路径-相对路径) Class classobj2 = Class.forName("fanshe.Student"); System.out.println(classobj2.getName()); //方式三(通过类名) Class classobj3 = Student.class; System.out.println(classobj3.getName()); } }
Java中的动态代理是什么?有哪些应用场景
如果不能确定被代理的是哪个类,那么可以使用类的动态加载机制,在代码运行期间加载被代理的类这就是动态代理,比如RPC框架和Spring AOP机制,加事务,加权限,加日志。
怎么实现动态代理
动态代理实现:首先必须定义一个接口,还要有一个InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)。利用到InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
package com.meituan.hyt.test3.service; public interface UserService { public String getName(int id); public Integer getAge(int id); } package com.meituan.hyt.test3.service.impl; import com.meituan.hyt.test3.service.UserService; public class UserServiceImpl implements UserService { @Override public String getName(int id) { System.out.println("------getName------"); return "Tom"; } @Override public Integer getAge(int id) { System.out.println("------getAge------"); return 10; } } package com.meituan.hyt.test3.jdk; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class MyInvocationHandler implements InvocationHandler { private Object target; MyInvocationHandler() { super(); } MyInvocationHandler(Object target) { super(); this.target = target; } @Override public Object invoke(Object o, Method method, Object[] args) throws Throwable { if("getName".equals(method.getName())){ System.out.println("++++++before " + method.getName() + "++++++"); Object result = method.invoke(target, args); System.out.println("++++++after " + method.getName() + "++++++"); return result; }else{ Object result = method.invoke(target, args); return result; } } } package com.meituan.hyt.test3.jdk; import com.meituan.hyt.test3.service.UserService; import com.meituan.hyt.test3.service.impl.UserServiceImpl; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class Main1 { public static void main(String[] args) { UserService userService = new UserServiceImpl(); InvocationHandler invocationHandler = new MyInvocationHandler(userService); UserService userServiceProxy = (UserService)Proxy.newProxyInstance(userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), invocationHandler); System.out.println(userServiceProxy.getName(1)); System.out.println(userServiceProxy.getAge(1)); } }
super关键字的作用?
1、super表示超(父)类的意思,this表示对象本身
2、super可用于访问父类被子类隐藏或着覆盖的方法和属性,使用形式为super.方法(属性)
3、在类的继承中,子类的构造方法中默认会有super()语句存在(默认隐藏),相当于执行父类的相应构造方法中的语句,若显式使用则必须位于类的第一行
4、对于父类有参的构造方法,super不能省略,否则无法访问父类的有参构造方法,使用形式为super(xx,xx...)
String为什么要设计为不可变类?
字符串常量池可以节省大量的内存空间。字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。其中常量池中存的是引用
不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
从jdk1.7开始,字符串常量池就开始放在堆中,然后本文的所有内容都是基于jdk1.8的
String str1 = "abc";//采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"abc"这个对象的引用 String str2 = "abc"; //如果不存在,则在堆中创建"abc"这个对象,并将其引用添加到字符串常量池(实际上是将引用放到哈希表中),随后将引用赋给str1 ///如果存在,则不创建任何对象,直接将池中"abc"对象的引用返回,赋给str2。因为str1、str2指向同一个对象,所以结果为true。 String str3 = new String("abc"); String str4 = new String("abc"); //采用new关键字新建一个字符串对象时,JVM首先在字符串池中查找有没有"abc"这个字符串对象的引用 //如果没有,则先在堆中创建一个"abc"字符串对象,并将引用添加到字符串常量池,随后将引用赋给str3 //如果有,则不往池中放"abc"对象的引用,直接在堆中创建一个"abc"字符串对象,然后将引用赋给str4。这样,str4就指向了堆中创建的这个"abc"字符串对象; //因为str3和str4指向的是不同的字符串对象,结果为false。 // true System.out.println(str1 == str2); // false System.out.println(str1 == str3); // false System.out.println(str3 == str4);
由于String类不可变的特性,所以经常被用作HashMap的key
什么是字符串常量池?
JDK1.7开始字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
String中创建字符串常量用new和不用new的区别
(本题是JDK1.7之前的字符串常量池位置)
第一种方式通过关键字new定义过程: 1.在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“myString”常量,节省内存空间。 2.然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s1”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。 3.你会发现String s1 = new String(“myString”); 这一句代码实际上可能创建了2个对象, 一个是String对象,存放在堆中, 一个是字符串常量对象,存放在串池中
第二种方式直接定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“myString”,如果不存在,则在常量池中开辟一个内存空间存放“myString”;如果存在的话,则不用重新开辟空间。然后在栈中开辟一块空间,命名为“s1”,存放的值为常量池“myString”的内存地址(相当于指向常量池中的“myString”)
String str="i"与 String str=new String(“i”)一样吗?
不一样,因为内存的分配方式不一样。String str="i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 String s = new String(“xyz”);创建了几个字符串对象两个对象,一个是静态区的"xyz",一个是用new创建在堆上的对象。
String、StringBuilder、StringBuffer的区别
String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。每次对String的操作都会生成新的String对象
private final char value[];
每次+操作 :隐式在堆上new了一个跟原字符串相同的StringBuilder对象,再调用append方法拼接+后面的字符。
StringBuffer与StringBuilder都继承了AbstractStringBulder类,而AbtractStringBuilder又实现了CharSequence接口,两个类都是用来进行字符串操作的。在做字符串拼接修改删除替换时,效率比string更高。
StringBuffer是线程安全的,StringBuilder是非线程安全的。所以Stringbuilder比stringbuffer效率更高,StringBuffer的方法大多都加了
synchronized关键字
String字符串修改实现的原理?
String的“+”拼接字符串,其真正实现的原理是中间通过建立临时的StringBuilder对象,然后调用append方法实现,然后再调用StringBuilder对象的toString方法将该StringBuilder对象转化为String对象,但StringBuilder对象的toString方法中是新new了一个String对象; StringBuilder对象的toString方法中是新new了一个String对象,所以str4是指向一个使用new新创建出来的一个对象,不会复用常量池中的对象;
final修饰StringBuffer后还可以append吗?
StringBuffer 理解为缓冲区,不能通过赋值符号对其进行赋值,使用append进行值修改。没有创建新的对象。
对其加以final修饰,fianl 修饰引用对象,代表引用对象不可变,StringBuffer 实现append()没有产生新的对象,所以可以。
Java中的IO流的分类?说出几个熟悉的实现类?
-
按照流的流向分,可以分为输入流和输出流;
-
按照操作单元划分,可以划分为字节流和字符流;
-
按照流的角色划分为节点流和处理流。
-
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
-
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
字节流和字符流有什么区别?
1.Java的字节流 InputStream是所有字节输入流的祖先,而OutputStream是所有字节输出流的祖先。 2.Java的字符流 Reader是所有读取字符串输入流的祖先,而Writer是所有输出字符串的祖先。
字符流只能处理字符类型(char,纯文本可以用字符流,比如汉字,传输de时候要查询编码表,得到汉字对应的字符),
而字节流可以处理任何类型(比如图片,视频,是以二进制传输的)
BIO、NIO、AIO有什么区别?
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。 NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。 AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
BIO:(通常在while循环中服务端会一直调用accept方法,等待连接请求,一旦接收到连接请求,就会建立通信套接字进行读写操作,此时不会接受其他客户端发来的请求,知道与当前客户端的操作执行完成为止。)
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
NIO介绍
同步非阻塞I/O模型,也是I/O多路复用的基础。
阻塞I/O模型:在Linux系统中,默认情况下所有的socket都是阻塞的,
非阻塞I/O模型:在Linux系统下,可以设置socket为非阻塞的
应用程序通过轮询的方式,反复调用recvfrom等待返回成功指示。
select和epoll的区别
-
select的句柄数目受限,在linux/posix_types.h头文件有这样的声明: #define __FD_SETSIZE 1024 表示select最多同时监听1024个fd。而epoll没有,它的限制是最大的打开文件句柄数目;
-
epoll的最大好处是不会随着FD的数目增长而降低效率,在selec中采用轮询处理,其中的数据结构类似一个数组的数据结构,而epoll是维护一个队列,直接看队列是不是空就可以了。epoll只会对”活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数(把这个句柄加入队列),其他idle状态句柄则不会,在这点上,epoll实现了一个”伪”AIO。但是如果绝大部分的I/O都是“活跃的”,每个I/O端口使用率很高的话,epoll效率不一定比select高(可能是要维护队列复杂);
-
使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
自定义类加载器
自定义类的应用场景:
-
加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
-
从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
-
以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
-
双亲委派模型
//双亲委派模型的工作过程源码 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
双亲委派模型的工作过程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
Java 序列化中如果有些字段不想进行序列化 怎么办
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。 transient 只能修饰变量,不能修饰类和方法。
Transient关键字作用
-
可以阻止所修饰的变量被序列化,在被反序列化时,transient修饰的变量被设为初始值,如int为0,对象类型为null。
-
服务端给客户端发送序列化数据时,对类似密码这类数据敏感,希望在对其序列化时能进行加密,客户端只有拥有密钥才能对其完成反序列化操作,获得实际数据。
Java8的十大新特性
-
接口可以拥有默认方法。
-
Lambda表达式
-
函数式接口(接口中只定义了一个方法)
-
方法与构造函数引用(::符号)
-
Lambda作用域
-
访问局部变量(在lambda表达式中访问局部变量)
-
访问对象字段与静态变量
-
访问接口的默认方法
-
DATE API
-
注解
-