文章目录
- Java基础篇
- 1.何为编程
- 2.Java的特性(特点)
- 3.JVM,JRE,JDK关系
- 4.什么是字节码,采用字节码最大的好处是什么?
- 5.Java有哪些数据类型及其包装类
- 6.Java中final,finally,finalize区别
- 7.关键字this和super的区别
- 8.关键字static
- 9.代码块
- 10.重载(Overload)和重写(Override)的区别与联系
- 11.面向对象的四大特性
- 12.面向对象的五大基本原则
- 13.面向对象和面向过程区别?
- 14.多态,Java语言是如何实现多态
- 15.Java静态绑定和动态绑定
- 16.接口(Interface)和抽象类(Abstract class)对比
- 17.hashCode()与equals()的关系
- 18.public、private、protected的区别,访问权限
- 19.Object类有哪些常见方法
- 20.Java中克隆对象的方式
- 21.String\StringBuffer\StringBuilder区别
- 22.Java异常体系
- 23.Java 泛型,类型擦除
- 24.Java中的Comparator与Comparable有什么不同
- 25.switch能否作用在byte、long、String上?
- 26.Java中参数传递进入方法,是值传递还是引用传递
- 27.基本类型和包装类型的区别
Java基础篇
以下,全是站在巨人的肩膀上学习总结所来,侵删
1.何为编程
编程就是让计算机为解决某个问题而使用某种 程序设计语言
编写的 程序代码
,并最终得到结果的过程。
我们将解决问题的思路、方法、手段通过计算机能够理解的形式告诉计算机,使得计算机按照指令去工作,完成某种特定的任务。这种人和计算机交流的过程就是编程。
2.Java的特性(特点)
- 简单易学(与C/C++语法接近)
- 支持网络编程且方便(为此而诞生)
- 支持多线程(同一线程并行执行多项任务)
- 面向对象(封装,基础,多态)
- 跨平台(Java虚拟机实现平台无关性)
- 健壮性(Java语言的强类型机制、异常处理、垃圾的自动收集等)
- 安全性(GC自动回收垃圾,强制类型检查,取消指针)
3.JVM,JRE,JDK关系
跨平台性及其原理:指的是Java语言编写的程序,一次编译后(成为源程序)可依赖于JVM在多个 不同的计算机系统平台上运行(一次编译,随处运行)。不同计算机系统平台的虚拟机不同。
JVM(Java Virtual Machine)
Java虚拟机,运行Java字节码的虚拟计算机系统,不同的平台有各自的JVM,从而实现Java跨平台原理。
JRE(Java Runtime Environment)
Java运行环境,**包括JVM
和Java核心类库(API)
**等。如果是运行一个开发好的Java程序,计算机中只需要有JRE即可。
JDK(Java Development Kit)
java开发工具,给开发人员使用,包含Java层序开发包
和JRE
。
4.什么是字节码,采用字节码最大的好处是什么?
编译型语言:源代码->机器语言,
只需一次编译
就可以把源代码编译成机器语言,后面的执行无需重新编译
,执行效率高,比较依赖编译器,跨平台性差。比如C/C++等解释型语言:源代码->中间代码->机器语言,源代码先翻译成中间代码,在由解释器对中间代码进行解释运行,每执行一次翻译一次,运行效率低,跨平台性好。如Python,Matlab等。
字节码
Java源代码经过JVM编译器编译后产生的文件叫字节码(.class)文件
,它只面向Java虚拟机,不面向任何处理器。
采用字节码的好处
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时有保留了解释型语言可移植的特点。字节码文件可在多种不同的计算机上运行。
Java应该是编译型-解释型语言,因为同时具备编译型和解释型两种特性。Java编译的过程就属于编译型语言特性,在class文件执行过程就是解释型语言特性(解释给机器执行)。
5.Java有哪些数据类型及其包装类
Java是强类型语言
,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。
强类型语言:要求变量的使用要严格定义,所有变量都必须先定义后使用。就比如Java。
弱类型语言:数据类型可以被忽略的语言,一个变量可以赋予不同数据类型的值。就比如JavaScript。
Java数据类分为:
- 基本数据类型: 有数值型(整数类型(byte,short,int,long),浮点类型(float,double))、字符型(char)、布尔型(boolean)
- 引用数据类型: 有类(class),接口(interface),数组([])
基本数据类型 | 大小(Byte) | 默认值 | 包装类 |
---|---|---|---|
byte | 1 | 0(byte) | Byte |
short | 2 | 0(short) | Short |
int | 4 | 0 | Integer |
long | 8(64位) | 0L | Long |
float | 4 | 0.0f | Float |
double | 8 | 0.0d | Double |
boolean | 1或4 | false | Boolean |
char | 2 | \u0000(null) | Character |
- boolean类型单独使用占4字节,在数组中占1字节。简单解释一下:在Jvm中boolean的被编译为int类型,也就是4字节,但是boolean类型的数组编译时按byte array来编译,所以就占1字节。详细看:https://docs.oracle.com/javase/specs/#22909。
- 基本数据类型存储在
栈
中,引用数据类型存储在堆
中,但是封装类属于类,也就是对象,在堆中创建存储,对象的引用在栈中创建。- 基本数据类型按值传递,封装类按引用传递,这里还可以谈到
==
和equals()
的区别。- long在32位系统下占4字节,64位系统下占8字节。
- 包装类型的默认值都是null。
这里对包装类进行一下扩展
针对以下情况,为什么?
public static void main(String[] args) {
Integer s1 = 100,s2=100,s3=129,s4=129;
System.out.println(s1==s2);//true
System.out.println(s3==s4);//false
}//我充满了❓
因为Integer/Long/Short有一个IntegerCache/LongCache/ShortCache,它们对-128~127
之间做了缓存,考虑到高频数值的复用场景,合理优化。IntegerCache的最大边界可以通过AutoBoxCacheMax进行配置。配合第27条学习理解。
6.Java中final,finally,finalize区别
-
final
final是修饰符也是关键字。可以
修饰类,变量,方法
。修饰类表示该类不能被继承;修饰方法表示该方法不能被继承(即不能被重写,但能重载);修饰变量表示该变量是一个常量,不能被修变。 -
finally
finally是关键字。一般作用在try-catch代码块中,通常将一定要执行的代码放在finally代码块中,表示不管是否出现异常都会执行,常用于关闭资源。
- try-catch块中finally不执行的情况:
- 对应的try块没有执行;
- 执行到try块时JVM关机;
- 如果finally中有return语句,则会覆盖try-catch块中的return语句,一般建议finallt中不要存在return语句。
- try-catch块中finally不执行的情况:
-
finalize
finalize()是属于Object类的protected方法。Object类是所有类的父类,该方法一般由GC调用,也是一个对象是否可回收的判断。
Java语言规范不能保证finalize()方法被及时的执行(单独的低优先级线程执行),更不会保证它一定执行。且finalize()只会被GC执行一次。
7.关键字this和super的区别
super:指向当前对象最近的父类的指针。
this:指向对象本身的指针。
区别
-
从本质上讲,this是一个指向本对象的指针, 然而super是一个
Java关键字
; -
super:它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参));
-
this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
super(),this()都是只能在构造函数中使用
-
super(参数):调用父类中某一个构造函数(应该为构造函数中的第一条语句) ;
-
this(参数):调用本类中另一个构造函数(应该为构造函数中的第一条语句);
-
super()和this()均需放在构造方法内第一行。每个子类构造方法的第一条语句,都是隐含地调用super(),会默认调用父类的无参构造,若父类没有无参构造方法,就会调用与子类对应参数的构造方法。
注意
- 尽管可以用this调用一个构造器,但却不能调用两个。
- this()和super()都指的是对象,所以均不可以在static环境中使用;
- this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
8.关键字static
static修饰的变量、方法是编译时静态绑定的,静态方法不可被重写(overrider)
-
创建独立于具体对象的变量或者方法,优先于对象存在的,以致于即使没有创建对象,也能使用属性和调用方法。static修饰的变量或方法是独立于该类的任何对象,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
-
可用来形成静态代码块优化程序性能,在类被初次加载的时候,会按照static块的顺序来执行每一个static块,并且
只会执行一次
,后面会根据需要可以再次赋值。静态修饰的属性 存储在方法区中
。JVM内存空间分为:分为栈、堆和方法区
-
static静态变量的生命周期持续到整个系统关闭。
-
静态只能访问静态,非静态可以访问静态和非静态。
构造方法、构造代码块、静态代码块的执行顺序是:
静态代码块->构造代码块->构造方法
9.代码块
-
普通代码块
在方法后面使用
{}
括起来的代码片段,不能单独执行,必须调用其方法明才能执行。 -
构造代码块
在类中没有任何的前缀或后缀,并使用
{}
括起来的代码片段。每执行一次构造方法,就执行一次构造代码块,执行在构造方法之前。(依托于构造函数)构造代码块的出现是为了提取构造函数的共同量,减少各个构造函数的代码。当遇到this()关键字时,不会再次执行构造代码块,但是super()不会,编译器只会把构造代码块插入到super()之后执行。(这里不能理解的话建议做一个代码测试)
-
静态代码块
在类中使用static修饰的代码块,并使用
{}
括起来,类第一次加载的时候执行一次,之后不再执行。第一个执行。用于静态变量的初始化或对象创建前的环境初始化。 -
同步代码块
使用
synchronize
关键字修饰的代码块,它表示在同一时间只能有一个线程进入到代码块块中,是一种多线程的保护机制。
构造方法、构造代码块、静态代码块执行顺序:静态代码块 -> 构造代码块 -> 构造方法.
10.重载(Overload)和重写(Override)的区别与联系
重载和重写都是实现多态的方式,区别在于重载是编译时多态,重写是运行时多态
数量不同的参数的构造方法就是重载
Overload | Override | |
---|---|---|
发生位置 | 一个类中 | 子父类中(extends/implements) |
参数列表 | 必须不同 | 必须相同 |
返回值类型 | 无限制 | 必须相同(相同/兼容) |
访问权限 | 无限制 | 子类访问权限不能低于父类的访问权限 (public>protected>default>private) |
异常处理 | 无限制 | 子类异常范围更小,且不能抛出新异常 |
多态 | 编译时多态 | 运行时多态 |
- 参数类型、参数个数、参数类型顺序不同 或 三者都不同,视为重载。
- 被重写的方法不能被private/static修饰,否则在其子类只是定义了一个新方法,并没有进行重写。
- 构造方法能被重载,不能被重写。
注:实现implements需要overrider父类的方法,继承extends不需要重写父类的方法。
11.面向对象的四大特性
多态性、封装性、继承性、抽象性。Java中只有单继承(extends),能够多实现(implements)。
-
封装性(encapsulation)
隐藏对象的属性和实现细节,仅对外公开 访问方式(如
get/set
方法),提高复用性
和安全性
。 -
继承性(extends)
从现有的类中派生出新的类,新的类可以增加
新的数据
或功能
,也可以使用父类的功能,但不能选择性的继承父类。实现代码的复用
。子类拥有父类非private的属性和方法;
子类在new的时候先在堆里面创建
父类的对象空间
,后把地址存储在子类的堆内存中,子类对象调取方法的时候先查看子类里面是否有对应的方法,再查看super的 -
多态性(polymorphism)
编译时多态:同一方法可根据对象的不同指向不同的代码(方法的重载);
运行时多态:也称动态链接。父类的引用指向子类对象,一个对象的实际类型确定,但是指向的引用类型不确定,提高了程序的拓展性。
Java中有继承(extends)、接口实现(implements)多态。
-
抽象性(abstract)
将一类对象的共同特征总结出来构造类的过程。
12.面向对象的五大基本原则
-
单一职责原则SRP(Single Responsibility Principle):类的功能得单一。
-
开放封闭原则OCP(Open-Close Principle):扩展开放,修改关闭。
-
里氏替换原则LSP(the Liskov Substitution Principle):子类可以替换父类出现在父类能够出现的地方。
-
依赖倒置原则DIP(the Dependency Inversion Principle):高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象,即
面向接口编程
。 -
接口分离原则ISP(the Interface Segregation):模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来。
13.面向对象和面向过程区别?
在探讨上面问题之前,先来谈谈对象叭,啊?我没对象怎么谈?傻瓜,你学会了面向对象我就给你发一个
面向过程就是把事物具体化,面向对象就是把事物抽象化。
面向过程
就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现。面向对象
是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
面向过程(Procedural oriented programming,POP)
以步骤划分
- 优点:性能比面向对象高,类调用需要实现实例化,比较消耗资源;比如单片机等,性能是最重要的因素。
- 缺点:没有面向对象易维护、易扩展、易复用。
面向对象(Object oriented programing,OOP)
以功能划分
- 优点:
易维护、易扩展、易复用
,由于拥有封装、继承、多态的特性,可以设计处低耦合的系统; - 缺点:性能比面向过程低。
面向对象的底层逻辑还是面向过程,把面向过程抽象成类,封装,方便使用就是面向对象了。
由于面向对象编程的三大特性,组织起来的代码结构是垂直结构的,无法定义水平结构的对象。框架中常用到AOP就实现了水平结构的体系。
14.多态,Java语言是如何实现多态
即:一千个读者就有一千个哈姆雷特
多态
即对象的多种形态。在一个父类有多个子类的前提下,同一个父类的多个对象,在接受到同一个参数时却产生了不同反应和效果。即父类的对象变量调用了不同子类中重写的方法。
多态分类
多态分为编译时多态
和运行时多态
。编译时多态是静态的,通过方法重载(Overload)实现;运行时多态是多态的,通过方法重写(override)实现,当然这发生在父子类之间。
编译时多态就是在调用同一个方法时,不用区分参数类型、个数或顺序,程序会自动执行相应的方法,比如在构造方法中传递不同的参数获取都能获取到对应的响应。
运行时多态是动态绑定
,使用父类引用
指向子类对象,再调用父类中的某一方法时,不同子类会表现出不同的结果。这样的好处就是扩展性极好,举一个栗子:方兴未艾的英雄联盟游戏中有不同的角色,假设它们都有一个共同的父类,它们作相同的动作时表现出来的效果就会不一样,比如回城方法,各种千奇百怪的动作,这就是都重写了父类中回城的方法,各自有自己的实现,表现出来父类回城这个方法的多态。如果出了新角色,只需要再写一个类继承父类,重写对应的方法就可以,其他代码不要做出很多改动,可维护性很好。
运行时动态一般都是使用
面向接口编程
来实现.
多态机制
程序中定义的引用变量
指向具体的类型
和 通过该引用变量发出的方法
调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个子类的实例对象,该引用变量发出的方法调用到底是哪个子类中实现的方法,必须在程序运行期间才能决定。
因为在程序运行时才能确定具体的类,所以不用修改源代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的代码,让程序可以选择多个运行状态,这就是多态。
多态的实现
Java实现多态有三个必要条件:继承、重写、对象类型转换。父类的对象变量可以引用本类的对象,也可以引用子类的对象;而子类的对象变量引用父类对象,父类对象必须向下强转为子类对象。
对象类型转换分为向下转型和向上转型:
- 向上转型:子类对象直接赋给父类的对象变量。也称之为类型的自动转换;
- 向下转型:将父类的引用强转为子类的引用。它必须强转,格式:
(类型名)对象变量
.
多态实现遵循的原则
当超类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过,被子类覆写过的方法。
对象引用和对象实例
new创建对象实例,对象实例存储在堆内存中,对象引用指向对象实例,对象引用存放在栈内存中。一个对象引用可以指向0/1个对象,一个对象实例可以有N个引用指向它
instanceof
这里说一下特殊运算符instanceof,它是Java语言中的二元运算符,它的作用是:判断一个引用类型变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例,即它左边的对象是否是它右边的类的实例,该运算符返回boolean类型的数据。
多态,一个Java使用者一种离不开的两个字,其实,一个人一生也是哈哈哈
上面提到动态绑定,下面就来介绍以下
15.Java静态绑定和动态绑定
在Java方法被调用的过程中,JVM是如何知道调用的是哪个类的方法源代码?这就涉及到程序绑定。程序绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对Java而言程序绑定分为
静态绑定
和动态绑定
,或者叫前期绑定
和后期绑定
。
静态绑定
在程序执行前此方法已经绑定,即在程序代码编译过程中就已知道这个方法到底是哪个类中的方法,此时由编译器或其它连接程序实现。针对Java可以简单的理解为程序编译器的绑定,且Java中的方法只有final,static,private和构造方法是静态绑定的。
分析:
- final修饰的方法能够被继承,但不能被重写,子类对象调用也是在执行父类中的代码块,将方法声明为final类型,可以避免方法被重写,能够有效的关闭Java中的动态绑定。
- static方法可以被子类继承,不能被子类重写(即覆盖),可以被子类隐藏。
被隐藏不被覆盖
的意思就是当子类中没有定义这个方法,子类调用此方法时会执行父类中的方法;而子类中定义了该方法,子类再次调用就执行子类中的方法;当子类向上转型的时,就总是执行父类中的此方法- private修饰的方法不能被继承,所以不能如果它的子类对象来调用,只能通过this来调用,就是private方法和定义这个方法的类绑定在了一起。
- 构造方法不能被继承,编译时可知道此构造方法属于哪个类。
通过上述分析,得出一个方法不可被继承
或者继承后不可被重写(覆盖)
,那么这个方法就采用了静态绑定。其他的都属于动态绑定。
动态绑定
在运行时根据具体对象的类型进行绑定。也就是说编译器此时仍然不知道对象的类型,但方法调用机制能够自己去调查(Java有instanceof关键字判断),找到正确的方法主体。动态绑定的过程:
虚拟机提取对象的实际类型的方法表 -> 虚拟机搜索方法签名 -> 调用方法
16.接口(Interface)和抽象类(Abstract class)对比
Inteface和abstract class是Java语言中对于抽象类定义进行支持的两种机制,赋予了Java强大的面向对象能力
接口中
所有的方法都是抽象的方法,是完全抽象的。抽象类中
的方法可以不是抽象的,是不完全抽象的。
抽象类必须被继承,抽象类不能直接new创建对象(限制开发者,不限制JVM实例化,在extends的时候就有一个super关键字)
相同点
-
接口和抽象类都不能实例化(new创建);
-
都用于被其他类实现或继承;
-
都包含了抽象方法,其子类都必须重写这些抽象方法。
不同点
参数 | 抽象类(Abstract) | 接口(Interface) |
---|---|---|
声明 | 使用abstract关键字声明 | 使用inteface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类 如果子类不是抽象类的话,它需要实现抽象类中所有声明的方法 | 子类使用implements关键字来实现接口 它需要实现接口中所有声明的方法 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口中抽象方法修饰符必须是public 非抽象方法可以定义其他修饰符private/protected |
多继承 | 一个类最多只能继承一个抽象类(防止方法错乱) | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段声明默认都是static和final |
作用
abstractclass是为了把相同的东西提取出来,即重用
;
Interface是为了把程序模块进行固化的契约,是为了降低偶合
。
接口可以继承接口,支持多继承,即
interface extends interface
.
17.hashCode()与equals()的关系
提到equals()就必须说一说==取equals()的区别:
- ==:在比较基本数据类型时,比较的是值,因为基本数据类型存储在栈区,没有引用地址;在比较应用数据类型时比较的是内存地址。
- equals():类有重写equals()方法时,根据重写方法进行比较(默认比较数据内容);类没有重写equals()方法时,等价于==。
hashCode()介绍
hashCode()的作用是获取哈希码,也称为散列码。返回int整数,用于确定(计算)该对象在哈希表中的索引位置。hashCode是定义在JDK的Object.java中,所有Java中任何类都包含hashCode()函数。
下面从性能
和可靠性
分析两者关系
性能:equals()既然已经有了对比功能,为什么要有hashCode()?
因为重写的equals()里的比较很全面、复杂,这样效率很低,而利用hashCode()进行对比,只要生成一个hash值就可以比较了,效率很高。hashCode()极大的缩小了数据需要equasl()的次数。
可靠性:hashCode()既然效率这么高,为什么还要equals()?
因为hashCode()并不完全可靠,有时候不同的对象生成的hash值也一样(生成的hash值取决于hash公式),所以hashCode()并不完全可靠,为了使得整体的可靠性,就得在hash值相等的基础之上使用equals()进行最后对比。
两者相辅相成:
我们以hashSet插入数据如何检查重复为例。把对象加入HashSet的时候,HashSet会先计算对象的hash值来判断对象加入的位置,同时也会与其他已经加入的对象的hash值就行比较,如果没有相符的hash,HashSet会认为对象没有重复出现。但是如果有重复的hash,这时会调用equals()检查对象是否真的相同。如果两者相同,HashSet不会让其加入操作成功;如果不能的话就会重新散列到其他位置。这样极大的减少了equals()的次数,提高了性能。
所以:
equals()为真
的两个对象它们通过hashCode()得出hash值一定相等,也就是equals()对比绝对可靠的;
hahsCode()得出的hash值相等的两个对象的equals()不一定真,也就是hashCode()不是绝对可靠的;
扩展:
- 只要重写equasl(),就必须重写hashCode();
- 一般地方不需要重写hashCode(),只有当类放在HashMap、HashSet等hash结构的集合中时才会重写hashCode()。
- 可通过hashCode()得到的hash值来访问小内存块,这里的小内存块就比如HashMap中的某一个桶中的数据链。
18.public、private、protected的区别,访问权限
作用域 | 当前类 | 同一个package | 子孙类 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
不写作用域默认就是default
19.Object类有哪些常见方法
Object类是所有类的父类,任何类都默认继承Objcet
变量和类型 | 方法 | 描述 |
---|---|---|
protected Object | clone() | 创建并返回此对象的副本(浅克隆) |
boolean | equals(Object obj) | 某个对象是否与之相等 |
Class<?> | getClass() | final方法,返回此Object的运行时类 |
int | hashCode() | 获取对象的hash |
void | notify() | 唤醒在该对象监视器上等待的单个线程 |
void | notifyAll() | 唤醒等待此对象监视器上的所有线程 |
String | toString() | 返回对象的字符串表示形式 |
void | wait() | 当前线程等待获得锁,直到获得锁或中断 |
void | wait(long timeoutMillis) | 设置一个超时时间(毫秒) |
void | wait(long timeoutMillis,int nanos) | 设置一个超时时间(毫秒+纳秒) |
线程调用wait()方法后进入睡眠状态,以下情况会被终止线程睡眠:
- 其他线程调用了该对象的notify()/notifyAll()方法;
- 其他线程调用了interrupt中断该线程;
- 时间间隔到了(如果有的话)。
上面既然提到clone()了,就延伸一点点
20.Java中克隆对象的方式
1.重写java.lang.Object类中的clone()方法
分为浅克隆(ShallowClone)和深克隆(DeepClone)。浅克隆和深克隆的区别在于是否支持引用类型
的成员变量的复制,浅克隆仅仅是把地址copy过去,深克隆是复制一份成员变量。浅克隆不彻底,深克隆彻底。
浅克隆:被克隆对象实现Cloneable接口,重写clone()方法。(retunr super.clone()
)
深克隆:被克隆对象和其成员变量都实现cloneable接口,都重写clone()方法。对象本身被复制,对象所包含的所有成员变量也将被复制。
2.工具类BeanUtil和PropertyUtils进行对象数据的复制
BeanUtils 提供对 Java 反射和自省 API 的包装,其主要目的是利用反射机制对 JavaBean 的属性进行处理,所以两者必须具有get/set方法。
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, (Class)null, (String[])null);
}
3.通过序列化实现对象复制
首先实现可序列化Serializable接口,通过序列化的对象就相当于复制了一份到流中(没有必要存储到本地),即使是原型对象的引用成员变量也相当于复制了一份,之后通过反序列化技术得到复制的对象。这是一种深度克隆机制。
21.String\StringBuffer\StringBuilder区别
String声明的是不可变的对象,每次操作都会生成新的String对象,然后将指针指向新的String对象,而StringBuffer和StringBuilder可以在原有对象基础上进行操作,所以经常改变的字符串内容最好不要使用String。
StringBuffer和StringBuilder最大的区别在于StringBuffer线程安全,StringBuilder线程不安全,且StringBuffer的性能低于StringBuilder,在多线程环境下使用StringBuffer,在单线程环境下使用StringBuilder。
更深层次的讨论放在JVM篇
22.Java异常体系
作图网站推荐:processon.com
异常类的继承结构图
Java中所有异常类都继承(extends)自Throwable
类,Throwable有两个子类Error
和Exception
。
Error
Error是程序无法处理的错误(Error也是一种异常
),一旦出现Error JVM会被迫停止运行。通常有Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)等。
Exception
Exception不会导致程序停止,程序本身可以捕获并且可以处理的异常。Exception又分为运行时异常
和编译异常
。
运行时异常(不可查异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。如NullPointerException,ArrayIndexOutBoundException。
编译异常(可查异常):Exception中除RuntimeException极其子类之外的异常。如IOException,必须对该类异常进行处理,否则编译不通过。通常不会自定义该类异常,而是直接使用系统提供的异常类。
不可查异常:unchecked exception,编译器编译时
检查不出来
并且不要求必须处理
的异常。该类异常包括运行时异常(RuntimeException极其子类)和错误(Error)。可查异常:checked exception,编译器在编译时
能检查出来
并要求必须处理
的异常。除RuntimeException及其子类外,其他的Exception异常都属于可查异常。
异常处理
Java中,异常的处理机制分为抛出异常
和捕获异常
。
抛出异常:该方法把异常类型和异常出现时的程序状态信息封装为异常对象,本应用把该异常抛出。
捕获异常:方法抛出异常后,系统自动根据该异常对象寻找合适异常处理器(Exception Handler)来处理该异常。所谓合适类型的异常处理器指的是异常对象类型和异常处理器类型一致。
对于不同的异常,Java采用不同的异常处理方式
- RuntimeException系统将自动抛出,应用本身可以选择处理或者忽略该异常。
- 对于方法中产生的Error,该异常一旦发生JVM将自行处理该异常,因此Java允许在应用不抛出此类异常。
- 对于所有的
可查异常
,必须进行捕获或者抛出该方法之外交给上层处理。也就是当一个方法存在异常时,要么使用try-catch捕获,要么使用该方法使用throws将该异常抛调用该方法的上层调用者。
throw和throws
可用throws/throw抛出异常
throws:用来声明
一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给谁。用在方法名后面,跟异常类名。有throws声明的方法不一定会抛出异常。
private int sum (int i, int j) throws RuntimeException, IOException {/**/}
throw:用来抛出一个具体的异常类型。用在方法体内,跟的是异常对象名。执行throw一定会抛出某种异常。
throw newExceptionType;
23.Java 泛型,类型擦除
泛型抽离了数据类型与代码逻辑,提高程序代码的简洁性和可读性,并提供可能的编译时类型转换安全检测功能。
23.1 泛型是什么
泛型,广泛适用的类型
,是JDK5引入的新特性,官方的说法是为了 参数化类型,就是将类型作为一个参数传递给一个方法或者传递给一个类。当具体的类型确定后,泛型又提供了一种类型检测机制,只有类型匹配的数据才能正常传递。
23.2 泛型的定义和使用
泛型按照使用情况分为3种:泛型类
、泛型方法
、泛型接口
。
Java建议我们用单个大写字母来代替类型参数,常见的如:
字母 描述 T 一般的任何类 E Element/Exception K Key V Value,与K配合使用 S Subtype,类型擦除
泛型类和泛型接口
//1.泛型类:单个参数
public class singleType<T> {...}//T就是类型的参数,等待传递节来
//2.泛型类:多个参数
public class MultiType<K , V> {...}
//3.泛型接口
public interface Iterble<T>{...}
泛型方法
//1.泛型方法
public class Test {
//1.无返回值的泛型方法
public <T> void testMethod1(T t){...}
//2.有返回值的泛型方法 <T>中的T表示类型参数 方法中的T表示参数化类型
public <T> T testMethod2(T t){...}
}
//2.泛型类+泛型方法(同名)
public class Test<T> {
public <T> T testMethod(T t){...}
}//此例 方法中的T与Test<T>中的T没有任何联系,为了方便区分,建议不使用同名
//3.泛型类+泛型方法(不同名)
public class Test<T> {
public <E> E testMethod(E t){...}
}//此例 方法中的T与Test<T>中的T没有任何联系,为了方便区分,建议不使用同名
//泛型方法始终以自己定义的类型参数为准
为什么泛型方法是
public <T> T testMethod(T t)
这样,而不能是public T testMethod(T t)
这样呢?因为<T>
表示声明
类型参数为T,也就是声明后面方法括号(T t)
中T
的类型,想象一下如果直接传递参数不能确定到底是接收哪一种类型的参数是不可取的。
23.3 通配符 ?
除了<T>
还有<?>
表示泛型,?
被称为通配符,通配符的出现就是为了指定泛型中的类型范围。它分为:<?>
无限定通配符、<? extends T>
有上限的通配符、<? super T>
有下限的通配符。
无限定通配符<?>
<?>
常与容器类
配合使用,它其中的 ?
代表的是未知类型,所以涉及到 ?
时的操作,一定与具体类型无关。<?>
提供了只读功能,它不关心元素数量(就是是ArrayList<?>
也不能插入数据)
<? extends T>
?
代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 T 及类型 T 的子类都可以。
23.4类型擦除
在介绍类型擦除前先看一个栗子:
System.out.println(new ArrayList<Integer>().getClass() == new ArrayList<String>().getClass());//true
对于熟悉和不熟悉的小伙伴立马就能给出答案,那么为什么是true呢,这就是
类型擦除
的作用。在JVM中都表示为List.class,而不是List.class或其他
由于泛型信息只存在与代码编译阶段,在进入JVM之前,与泛型相关的信息
会被擦除掉,即类型擦除
. 并且在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限(<?>
),如则会被转译成普通的 Object 类型,如果指定了上限(<? extends Sting>
)则类型参数就被替换成类型上限String
23.5类型擦除带来的局限性
类型擦除,是泛型能够与之前的 Java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样也能让我们绕过泛型本身的一些限制。比如:
正常情况下因为泛型的限制,因为类型不匹配,最后一行代码会不编译通过,但是基于对类型擦除的理解(即在编JVM中Integer
类型变为了Object
),就可以利用反射原理绕过这一限制。利用反射绕过编译器调用add
方法
public class Main {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> list = new ArrayList<>();
list.add(1);
//list.add("abc");
Method method = list.getClass().getDeclaredMethod("add",Object.class);// throws NoSuchMethodException
method.invoke(list,"abc");// throws InvocationTargetException, IllegalAccessException
for(Object obj : list){
System.out.println(obj);
}
}
}
打印的数据
1
abc
可以看见利用类型擦除原理,用反射的手段就完美绕开了正常开发中编译器不允许的操作限制。
23.6泛型中值得注意的地方
泛型不接受8种基本数据类型,它们是:byte,short,char,int,long,float,double,boolean
,需要使用它们的包装类。
24.Java中的Comparator与Comparable有什么不同
两者都是java.util包下的接口
Comparable用于定义对象的自然顺序,是排序接口(内部比较器),而comparator通常用于定义用户定制的顺序,是比较接口(外部比较器)。我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接口),那么我们就可以建立一个“该类的比较器”来进行排序。comparable总是只有一个,但是可以有多个comparator来定义对象的顺序。
25.switch能否作用在byte、long、String上?
JDK8支持中switch(expr)
中的expr
支持byte,short,char,int,enum,String
,long类型不支持。
26.Java中参数传递进入方法,是值传递还是引用传递
对于数组而言是引用传递
。比如在对数组进行排序的时,排序方法都是没有返回值的void
;
对于对象而言是值传递
。在对象参数传递进入方法时,需要获取被改变的对象,就得return出去。
27.基本类型和包装类型的区别
Java的每个基本类型都对应了一个包装类型(int的包装类型就是Integer),它们之间的区别主要有四点:
27.1 包装类型可以为null,基本类型不可以
这使得包装类型可以应用于POJO中,而基本类型不行。
- POJO:Plain Ordinary Java Object,普通的Java对象,只有属性字段以及setter/getter方法;
- DTO:Data Transfer Object,数据传输对象,用于表现层与服务层之间的数据传输对象;
- VO:View Object,视图对象,把某个页面的数据封装起来;
- PO:Persistent Object,持久化对象,可以看成是数据库中的表映射的Java对象。
为什么POJO的属性必须要用包装类型?《阿里巴巴Java开发手册》中写到:
数据库的查询结果可能是null
,如果使用基本类型的话,因为要自动拆箱,就会抛出NullPointerException
。
27.2 包装类型可用于泛型,而基本数据类型不可以
泛型在编译时会继续类型擦除,最后只保留原始类型,所谓的原始类型就是在写泛型时的最高父类(没有父类就是Object类)。比如Persion<? extends HH>
,那么Persion
的最高父类就是HH
。泛型不支持使用基本数据类型,因为使用基本数据类型时会编译出错。
27.3 基本类型比包装类型更高效
基本类型直接存储在栈中,而包装类型则存储在堆中(栈中有引用)。基本类型更节省内存空间,另外,如果我们存储一般的数值,没有基本类型每一次都需要new对象,是一件很麻烦的事情。
27.4 两个包装类型的值相等,但却不同
Integer chenmo = new Integer(10);
Integer wanger = new Integer(10);
System.out.println(chenmo == wanger); // false
System.out.println(chenmo.equals(wanger )); // true
假设大家都明白==
和equals()
的区别,就==
比较的是对象地址,而new
了两次对象,创建了两次内存空间,地址比较肯定是false,但是它们的值是相同的。
27.5 自动装箱和自动拆箱
基本类型转换为包装类型的过程叫做装箱(boxing)
,包装类型转换为基本类型的过程叫做拆箱(unboxing)
。Java提高自动装箱与自动拆箱的功能。
Integer chenmo = 10; // 自动装箱 int自动装箱为Integer
int wanger = chenmo; // 自动拆箱 Integer自动拆箱成int
自动装箱是通过Integer.valueOf()
实现的,自动拆箱是通过Integer.intValue()
实现的。
值都学习的是:
- 当使用
==
比较基本类型和包装类型时,包装类型会自动拆箱为基本数据类型;- 当需要进行自动装箱时,如果数字在
-128
至127
之间时,会直接使用缓存中的对象,而不是重新创建一个对象(由于IntegerCache的作用);- 当然在编写代码的时候,避开不必要的自动装箱和自动拆箱的过程,避免不必要的性能浪费。