数据类型
基本类型:
1、6 种数字类型 :整数 (byte,short,int,long)、浮点数(float,double)
2、1 种字符类型:(char)
3、1 种布尔型: (boolean)
这 8 种基本数据类型的默认值以及所占空间的大小如下:
基本类型 | 内存占用 | 取值范围 | 默认值 | 包装类(不赋值为 Null ) |
---|---|---|---|---|
byte | 1字节(byte),8位(bit) | -128~127 | 0 | Byte |
short | 2字节(byte),16位(bit) | -32768~32767 | 0 | Short |
int | 4字节(byte),32位(bit) | $-2^{31} $ ~ 2 31 − 1 2^{31}-1 231−1 | 0 | Integer |
long | 8字节(byte),64位(bit) | − 2 63 -2^{63} −263 ~ 2 63 − 1 2^{63}-1 263−1 | 0L | Long |
float | 4字节(byte),32位(bit) | 1.4013E-45 ~ 3.4028E+38 | 0f | Float |
double | 8字节(byte),64位(bit) | 4.9E-324 ~ 1.7977E+308 | 0d | Double |
char | 2字节(byte),16位(bit) | 0-65535 | ‘u0000’ | Character |
boolean | 逻辑上理解是占用 1 位 | true,false | false | Boolean |
另外,对于 boolean
,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。
补:
1、二进制数系统中,每个0或1就是一个位,叫做bit(比特)。
2、字节 是我们常见的计算机中最小存储单元。
8个bit(二进制位) 0000-0000表示为1个字节,写成1 byte或者1 B。
3、1byte=8bits
注意:
1、Java中的默认类型:整数类型是 int 、浮点类型是 double 。
2、Java 里使用 long
类型的数据一定要在数值后面加上 L,否则将作为整型解析。
3、char a = 'h'
,char :单引号;String a = "hello"
,字符串双引号。
4、JVM 层面来分析。
基本数据类型 直接存放在 Java 虚拟机栈的局部变量表中,
包装类型 属于对象类型,存在于堆中。相比于对象类型, 基本数据类型占用的空间非常小。
引用类型
数组,类,接口 ,字符串…除了8种基本类型外,其余都是引用类型
引用类型在内存中保存的是 地址值
类型转换
自动转换:
取值范围小的类型 自动提升为 取值范围大的类型 。
byte、short、char —> int —> long —> float —> double
强制转换:
将 取值范围大的类型 强制转换成 取值范围小的类型 。损失精度,直接舍弃小数位,无四舍五入。
数据类型 变量名 = (数据类型)被转数据值;
int i = 1;
byte b = 2;
// byte x = b + i; // 报错:int类型和byte类型运算,结果是int类型
int j = b + i;
byte b1 = 1;
byte b2 = 2;
//在编译的时候,已经确定了 1+2 的结果并没 有超过byte类型的取值范围,所以 byte = byte + byte
byte b3 = 1 + 2;
/*
b1 和 b2 是变量,变量的值是可能变化的,在编译的时候,编译器javac不确定b1+b2的结果是什么,因此会将结果以int类型进行处理,所以int类型不能赋值给byte类型,因此编译失败。
编译失败,爆红,b1 + b2结果应该是Java默认的整数类型int。
*/
byte b4 = b1 + b2;
运算符
1、i++ * i
:i先等于1,再自加,然后接着计算*
// ++i: i先自加再运算;i++:先运算再自加。
int i = 1; //假设以下i初始都等于1
j = i++; //j=1 i=2
int result1 = i++ * i; //1*2=2 i=2
int result2 = i* i++ ; //1*1=1 i=2
int result3 = (i++)*(++i)*(i++)*(++i)*(i++); //1*3*3*5*5=225 i=6
2、+
在遇到字符串的时候,表示连接、拼接的含义。
System.out.println("5+5="+5+5); //输出 5+5=55
System.out.println(5+5); //输出 10
3、&
和&&
的区别:
&和&&运算结果是一样的。
&&
:逻辑与运算符。
具有短路性,如果第一个表达式为 false,则直接返回 false。
&
:按位与运算符、逻辑与运算符。
不存在短路性,不管左边的表达式是true还是false,右边的表达式一定会执行。
所在通常使用逻辑与运算符都会使用 &&,而 & 更多的适用于位运算。
4、+=
//编译报错:计算结果被提升为int类型,再向short类型赋值时发生错误
short i = 1; i = i+1;
//正常编译和执行:+= 是一个运算符,只运算一次,并带有强制转换的特点。
//也就是说 s += 1 相当于 s = (short)(s + 1)
short i = 1; i += 1;
权限修饰符和一些常用的关键字
-
public:公共的。 被其修饰的类、属性以及方法:不仅可以跨类访问,而且允许跨包(package)访问。
-
protected:受保护的 。被其修饰的类、属性以及方法:只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。
-
default:默认的 。默认权限(同包权限)即:包内可访问,包外不可访问,不管是子类还是没有继承关系的类。
-
private:私有的 。只能在当前类中使用,外部不能访问。
final 关键字
final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:
- final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
- final 修饰的方法不能被重写;
- final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
说明:使用 final 方法的原因有两个。
- 第一个原因是把方法锁定,以防任何继承类修改它的含义;
- 第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。
static 关键字
static 关键字主要有以下四种使用场景:
1、修饰成员变量和成员方法:
被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
调用格式:类名.静态变量名
类名.静态方法名()
2、静态代码块:
静态代码块
定义在 类中方法外,一个类中的静态代码块可以有多个,位置可以随便放 。 该类不管创建多少对象,静态代码块只执行一次。
static {
语句体;
}
JVM 加载类时会执行这些静态的代码块,如果静态代码块有多个,JVM 将按照它们在类中出现的先后顺序依次执行,每个代码块只会被执行一次。
静态代码块 和非静态代码块的区别:
相同点:
都是在 JVM 加载类时且在构造方法执行之前执行,在类中都可以定义多个,定义多个时按定义的顺序执行,一般在代码块中对一些 static 变量进行赋值。
不同点:
静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。
静态代码块只在第一次 new 执行一次,之后不再执行,而非静态代码块在每 new 一次就执行一次。
非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
🐛 修正(参见: issue #677) :静态代码块可能在第一次 new 对象的时候执行,但不一定只在第一次 new 的时候执行。比如通过
Class.forName("ClassDemo")
创建 Class 对象的时候也会执行,即 new 或者Class.forName("ClassDemo")
都会执行静态代码块。
public class Test {
public Test() {
System.out.print("默认构造方法!--");
}
//非静态代码块
{
System.out.print("非静态代码块!--");
}
//静态代码块
static {
System.out.print("静态代码块!--");
}
private static void test() {
System.out.print("静态方法中的内容! --");
{
System.out.print("静态方法中的代码块!--");
}
}
public static void main(String[] args) {
Test test = new Test(); //静态代码块!--非静态代码块!--默认构造方法!--
Test.test(); //静态代码块!--静态方法中的内容! --静态方法中的代码块!--
}
}
//静态代码块!--非静态代码块!--默认构造方法!--静态方法中的内容! --静态方法中的代码块!--
静态代码块(类加载时的初始化阶段) --> 非静态代码块(new创建对象实例前) --> 构造方法(new创建对象实例)
非静态代码块与构造函数的区别:
非静态代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化。
因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。
3、静态内部类(static 修饰类的话只能修饰内部类):
静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:
它的创建是不需要依赖外围类的创建。
它不能使用任何外围类的非 static 成员变量和方法。
//静态内部类实现单例模式
public class Singleton {
//声明为 private 避免调用默认构造方法创建对象
private Singleton() {
}
// 声明为 private 表明静态内部该类只能在该 Singleton 类中被访问
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
//当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。
//只有当调用 getUniqueInstance()方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。
//这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
4、静态导包(用来导入类中的静态资源,1.5 之后的新特性):
格式为:import static
这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
//将Math中的所有静态资源导入,这时候可以直接使用里面的静态方法,而不用通过类名进行调用
//如果只想导入单一某个静态方法,只需要将*换成对应的方法名即可
import static java.lang.Math.*;
//换成import static java.lang.Math.max;具有一样的效果
public class Demo {
public static void main(String[] args) {
int max = max(1,2);
System.out.println(max);
}
}
静态方法与非静态方法
静态方法属于类本身,非静态方法属于从该类生成的每个对象。 如果您的方法执行的操作不依赖于其类的各个变量和方法,请将其设置为静态(这将使程序的占用空间更小)。 否则,它应该是非静态的。
总结:
- 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。
在一个静态方法内调用一个非静态成员为什么是非法的
【静态方法只能访问:静态成员变量(类变量)和静态方法,不能直接访问 普通变量和成员方法】
(静态只能访问 静态)
这个需要结合 JVM 的相关知识,静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。
在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
private关键字
如果变量或者方法被 private 则代表该属性或者该方法只能在类的内部被访问而不能在类的外部被访问。
Java 语言有哪些特点
- 面向对象(封装,继承,多态);
- 平台无关性( Java 虚拟机实现平台无关性);
- 支持多线程;
- 可靠性、安全性;
- 支持网络编程并且很方便;
- 编译与解释并存;(…)
Java 语言 “编译与解释并存“
编译型语言 是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;
解释型语言 是指解释器对源程序逐行解释成特定平台的机器码并立即执行。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释,在运行 三个步骤。
java程序从源代码到运行:
1、Java 程序 先经过JDK中的javac 编译,生成字节码.class
文件;
2、再通过类加载器把字节码文件加载进JVM虚拟机内存,然后通过解释器逐行解释执行;
3、最后转化成机器可执行的二进制机器码。
因此,我们可以认为 Java 语言编译与解释并存。
面向对象
(1)、“面向过程”vs“面向对象”
我觉得这两者是思考角度的差异,面向过程更多是以执行者的角度来思考问题,而面向对象更多是以组织者的角度来思考问题。
比如,扫教室这件事。老师叫小刘去打扫教室(面向对象),小刘就扫地、擦窗户一步步来完成(面向过程)。
面向过程 是,怎么一步步来完成这件事。
面向对象 是,谁来帮我们做这件事。
(2)、面向对象的三个基本特征:封装、继承、多态
封装:封装是指把一个对象的状态信息(属性、方法)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
1、使用 private 关键字来修饰成员变量。
2、对需要访问的成员变量,提供对应的一对 getXxx 方法 、 setXxx 方法。
【被private修饰后的成员变量和成员方法,只能在本类中访问。可提供 getXxx 方法 / setXxx 方法 供外界访问】
继承:让某个类型的对象获得另一个类型的对象的属性的方法。继承就是子类继承父类的特征和行为,使得子类对象具有父类的实例的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法)。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- Java只支持单继承,不支持多继承。
- 在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。
目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构 造方法调用时,一定先调用父类的构造方法。
//1、重名成员变量访问
super.父类成员变量/成员方法
this.子类(本类)成员变量/成员方法
//2、覆盖重写
//子类方法覆盖父类方法,必须要保证权限大于等于父类权限
//子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。
@Overide
public void showNum(){
//覆盖重写时,也可调用父类的成员方法
super.showNum();
}
//3、调用方法时,先在子类找,找不到再向父类找(向上找)
//4、构造方法
//构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
//构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构 造方法中默认有一个 super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。
class Zi extends Fu {
Zi(){
// super(),调用父类构造方法。默认有,不写也可
super();
}
}
//在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。
//super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。
多态:表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例。
多态存在的3个条件:
-
继承或者实现【二选一】
-
方法的重写【意义体现:不重写,无意义】
-
父类引用指向子类对象【比如:左父右子 】
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用 “只在子类存在但在父类不存在” 的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
多态:
父类类型 变量名 = new 子类对象; //右侧子类对象就被当作父类进行使用
//父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
//父类
public abstract class Animal {
public abstract void eat();
}
//子类
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
//测试类
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Animal a1 = new Cat();
a1.eat(); // 调用的是 Cat 的 eat
// 多态形式,创建对象
Animal a2 = new Dog();
a2.eat(); // 调用的是 Dog 的 eat
}
// 执行的是子类重写的方法。(看new的是谁,就执行谁的方法)
}
父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
转型:
向上转型:右侧子类对象就被当作父类进行使用
向下转型:多态不能调用 “只在子类存在但在父类不存在” 的方法。
多态,左父右子。子类可用父类的方法。方法只在子类存在但在父类不存在,就需要向下转型。
//**向上转型**:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。
父类类型 变量名 = new 子类类型(); //如:Animal a = new Cat();
//向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。
子类类型 变量名 = (子类类型) 父类变量名; //如: Cat c =(Cat) a;
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。
也就是说,不能调用子类拥有,而父类没有的方法。
所以,想要调用子类特有的方法,必须做向下转型。
转型的异常:
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a; //创建的是 Cat类型对象,这儿却 转换成了Dog对象
d.eat(); // 调用的是 Dog 的 eat 【运行报错: 类型转换异常ClassCastException】
}
}
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.eat(); // 调用的是 Cat 的 eat
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.eat(); // 调用的是 Dog 的 eat
}
}
}
接口和抽象类的区别
总结:
**(1)、抽象类 ** abstract
关键字
abstract
修饰方法,修饰类
抽象方法 : 没有方法体的方法。
抽象类:包含抽象方法的类。
1、抽象类中,可以有构造方法。不能创建对象。
2、继承抽象类的子类必须重写父类所有的抽象方法。不然,该子类也必须声明为抽象类。
3、抽象类中,不一定包含抽象方法,但是有抽象方法的。类必定是抽象类
(2)、接口 interface
关键字
如果说类的内部封装了成员变量、构造方法和成员方法,那么
接口的内部主要就是封装了方法,包含抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法
(JDK 9)。
public interface 接口名称 {
// 1、抽象方法
使用 abstract 关键字修饰,可以省略,没有方法体。该方法供子类实现使用。
// 2、默认方法
使用 default 修饰,不可省略,供子类调用或者子类重写。
可以继承,可以重写,二选一,但是只能通过实现类的对象来调用。
// 3、静态方法
使用 static 修饰,供接口直接调用。
静态与.class 文件相关,只能使用接口名调用,不可以通过实现类的类名或者实现类的对象调用。
// 4、私有方法
使用 private 修饰,供接口中的默认方法或者静态方法调用。
私有方法:只有默认方法可以调用。 私有静态方法:默认方法和静态方法可以调用。
}
1、接口中,没有构造方法。不能创建对象。
2、实现接口的类(可以看做 是接口的子类),必须重写接口中所有的抽象方法,否则它必须是一个抽象类。
3、接口中,没有静态代码块。
4、接口中,无法定义成员变量,但是可以定义常量,其值不可以改变,默认使用public static final
修饰。
5、接口可以多继承和 多实现
public class C implements A,B{
接口中有多个抽象方法时,实现类(非抽象类)必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次。
接口中有多个默认方法时,实现类都可继承使用。如果默认方法有重名的,必须重写一次
接口中存在同名的静态方法并不会冲突,原因是只能通过各自接口名访问静态方法。
当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的默认方法重名,子类就近选择执 行父类的成员方法。(class C extends D implements A,先D再A)
子接口 重写默认方法时,default关键字可以保留。 (继承)
子类 重写默认方法时,default关键字不可以保留。 (实现)
区别:
相同点
都不能被实例化 。实现接口或者继承抽象类后才能实例化。
单继承(重写抽象方法…)多实现
重载和覆盖重写
方法:
1、方法必须定义在 类中方法外
2、方法不能定义在另一个方法的里面(可以调用 )
普通方法:public static void 方法名
成员方法:public void 方法名
构造方法:public 方法名
1. 构造方法的名称必须和所在的类名称完全一样。
2. 构造方法不能return一个具体的返回值
3. 如果没有编写任何构造方法,那么编译器将会默认赠送一个无参构造方法。一旦编写了至少一个构造方法,那么编译器将不再赠送。
4. 构造方法也是可以进行重载的。重载:方法名称相同,参数列表不同。
构造方法主要作用是完成对类对象的初始化工作。
/*
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
所以我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
**方法重载:**重载发生在编译期,同样的一个方法能够根据输入数据的不同,做出不同的处理。
方法名相同,方法列表不同。(个数不同、数据类型不同、顺序不同。与修饰符和返回类型无关)
public static void open(){}
public static void open(int a){}
static void open(int a,int b){}
public static void open(double a,int b){}
public static void open(int a,double b){}
public void open(int i,double d){} //不是有效重载
public static void OPEN(){} // 代码严格区分大小写,所以不会报错。但是不是同名,所以不是有效重载
public static void open(int i,int j){} //不是有效重载
综上:重载就是同一个类中多个同名方法根据 不同的传参 来执行不同的逻辑处理。
**方法覆盖重写:**重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
1、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
2、如果父类方法访问修饰符为 private/final/static
则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
3、如果方法的返回值类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。
4、构造方法可以重载,但无法被重写。
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
成员变量和局部变量
1、语法形式上不同
成员变量:类中,方法外 。可以被 public
,private
,static
等修饰符所修饰
局部变量:代码块、方法中定义的变量 或者是方法的参数。不能被访问控制修饰符及 static
所修饰;
但是,成员变量和局部变量都能被 final
所修饰。
2、初始化值的不同
成员变量:有默认值
局部变量:**没有默认值。必须先定义,赋值,最后使用 **
3、在内存中的位置不同
成员变量:堆内存
局部变量:栈内存
4、生命周期不同
成员变量:随着对象的创建而存在,随着对象的消失而消失
局部变量:随着方法的调用而存在,随着方法的调用完毕而消失
内部类
内部类:
访问特点:
内部类可以直接访问外部类的成员,包括私有成员。
外部类要访问内部类的成员,必须要建立内部类的对象。
其他类访问内部类的 创建内部类对象格式:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
Java 和 C++的区别
java 底层是C、C++写的
Java早期的名字:C+±- 即:Java = C++:去掉繁琐的东西(指针、内存管理~
相同:
1、都是面向对象的语言,都支持封装、继承和多态
不同:
2、Java 不提供指针来直接访问内存,程序内存更加安全
3、Java 有自动内存管理 垃圾回收机制(GC),不需要程序员手动释放无用内存。
4、Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
5、C ++同时支持方法重载 和 操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)。
为什么 Java 中只有值传递
参考:https://www.zhihu.com/question/385114001/answer/1393887646
值传递:调用函数时,将实际参数值复制一份 传递到被调用函数中,在被调函数中修改参数值不会影响原实参值。
引用传递:调用函数时,将实际参数的地址 直接传递到被调用的函数中,在被调函数中修改参数值会影响原实参值。
Java 程序设计语言总是采用按值调用。方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
(1)、基础:
基本类型的变量存储的都是实际的值,
而引用类型的变量存储的是对象的引用——指向了对象在内存中的地址。
值和引用存储在 stack(栈)中,而对象存储在 heap(堆)中。
之所以有这个区别,是因为:
1、栈的优势是,存取速度比堆要快,仅次于直接位于 CPU 中的寄存器。但缺点是,栈中的数据大小与生存周期必须是确定的。
2、堆的优势是可以动态地分配内存大小,生存周期也不必事先告诉编译器,Java 的垃圾回收器会自动收走那些不再使用的数据。但由于要在运行时动态分配内存,存取速度较慢。
(2)、基本类型的参数传递
public class PrimitiveTypeDemo {
public static void main(String[] args) {
int age = 18;
modify(age);
System.out.println(age);
}
private static void modify(int age1) {
age1 = 30;
}
}
-
main 方法中的 age 是基本类型,所以它的值 18 直接存储在栈中。
-
调用
modify()
方法的时候,将为实参 age 创建一个副本(形参 age1),它的值也为 18,不过是在栈中的其他位置。 -
对形参 age1 的任何修改都只会影响它自身而不会影响实参。
(3)、引用类型的参数传递
public class ReferenceTypeDemo {
public static void main(String[] args) {
//对象引用a、b存储在栈中,保存了对象在堆中的地址。
//一旦new,就是在堆中开辟了一块新内存
Writer a = new Writer(18);
Writer b = new Writer(18);
modify(a, b);
System.out.println(a.getAge()); //30
System.out.println(b.getAge()); //18,不变
}
private static void modify(Writer a1, Writer b1) {
//修改参数的属性
a1.setAge(30);
//修改参数值
b1 = new Writer(18);
b1.setAge(30);
}
}
//看前面值传递、引用传递的定义—— ...在被调函数中**修改参数值**会影响.....
//对于引用传递,他的参数值是 Writer对象(此处)
//所以,修改参数值,是指修改为另一个 Writer对象, 而不是修改对象的属性
- 在调用
modify()
方法之前,实参 a 和 b 指向的对象是不一样的。
- 在调用
modify()
方法时,实参 a 和 b 都在栈中创建了一个新的副本,分别是 a1 和 b1,但指向的对象是一致的(a 和 a1 指向对象 a,b 和 b1 指向对象 b)。
- 在
modify()
方法中,修改了形参 a1 的 age 为 30,意味着对象 a 的 age 从 18 变成了 30,而实参 a 指向的也是对象 a,所以 a 的 age 也变成了 30;形参 b1 指向了一个新的对象,随后 b1 的 age 被修改为 30。
修改 a1 的 age,意味着同时修改了 a 的 age,因为它们指向的对象是一个; 修改 b1 的 age,对 b 却没有影响,因为它们指向的对象是两个。
程序输出的结果如下所示:
30
18
深拷贝和浅拷贝
原文链接:https://blog.youkuaiyun.com/riemann_/article/details/87217229
(1)、引用拷贝
创建一个指向对象的引用变量的拷贝
public class QuoteCopy {
public static void main(String[] args) {
Teacher teacher = new Teacher("riemann", 28);
Teacher otherTeacher = teacher;
System.out.println(teacher); //com.test.Teacher@28a418fc
System.out.println(otherTeacher); //com.test.Teacher@28a418fc
}
}
@Data //提供类的get、set、equals、hashCode、toString方法
@AllArgsConstructor
@NoArgsConstructor
class Teacher {
private String name;
private int age;
}
//输出地址值相同,那么它们肯定是同一个对象。teacher和otherTeacher的只是引用而已,他们都指向了一个相同的对象Teacher(“riemann”,28)。 这就叫做 引用拷贝。
(2)、对象拷贝
创建对象本身的一个副本。
public class ObjectCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("riemann", 28);
try {
otherTeacher = (Teacher) teacher.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(teacher); //com.test.Teacher@28a418fc
System.out.println(otherTeacher); //com.test.Teacher@5305068a
}
}
@Data //提供类的get、set、equals、hashCode、toString方法
@AllArgsConstructor
@NoArgsConstructor
class Teacher implements Cloneable {
private String name;
private int age;
@Override
public Object clone() throws CloneNotSupportedException {
Object object = super.clone();
return object;
}
}
/*
输出地址不同,也就是说创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量,这就叫做 对象拷贝。
注:深拷贝和浅拷贝都是对象拷贝
(3)、浅拷贝
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
public class ShallowCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("riemann", 28);
Student student1 = new Student("edgar",18,teacher);
Student student2 = null;
try {
student2 = (Student) student1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//拷贝后
System.out.println(student2);
//Student(name=edgar, age=18, teacher=Teacher(name=riemann, age=28))
// 修改老师的信息
teacher.setName("jack");
System.out.println(student1.getTeacher().getName()); //jack
System.out.println(student2.getTeacher().getName()); //jack
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Teacher implements Cloneable {
private String name;
private int age;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Student implements Cloneable {
private String name;
private int age;
private Teacher teacher;
@Override
public Object clone() throws CloneNotSupportedException {
Object object = super.clone();
return object;
}
}
/*
两个引用student1和student2指向不同的两个对象,但是两个引用student1和student2中的两个teacher引用指向的是同一个对象,所以说明是浅拷贝。
(4)、深拷贝
深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象 所引用的对象都复制了一遍。
public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("riemann",28);
Student student1 = new Student("edgar",18,teacher);
Student student2 = null;
try {
student2 = (Student) student1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//拷贝后
System.out.println(student2);
//Student(name=edgar, age=18, teacher=Teacher(name=riemann, age=28))
// 修改老师的信息
teacher.setName("jack");
System.out.println(student1.getTeacher().getName()); //jack
System.out.println(student2.getTeacher().getName()); //riemann
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Teacher implements Cloneable {
private String name;
private int age;
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Student implements Cloneable {
private String name;
private int age;
private Teacher teacher;
public Object clone() throws CloneNotSupportedException {
// 浅复制时:
// Object object = super.clone();
// return object;
// 改为深复制:
Student student = (Student) super.clone();
// 本来是浅复制,现在将Teacher对象复制一份并重新set进来
student.setTeacher((Teacher) student.getTeacher().clone());
return student;
}
}
/*
两个引用student1和student2指向不同的两个对象,两个引用student1和student2中的两个teacher引用指向的是两个对象,但对teacher对象的修改只能影响student1对象,所以说是深拷贝。
JDK,JRE,JVM有什么区别
JDK(Java Development Kit ):
程序员使用java语言编写java程序所需的开发工具包。它能够 创建和编译程序。
JRE(Java Runtime Environment):
java运行时环境。是java程序运行所需要的软件环境,是提供给想运行java程序的用户使用的。
包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。
如果你需要运行java程序,只需安装JRE就可以了。如果你需要编写java程序,则需要安装JDK。
但是,这不是绝对的。有时,即使您不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,需要在应用程序服务器中运行 Java 程序。应用程序服务器 会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。
JVM:
运行 Java 字节码的虚拟机。
Java 虚拟机实现平台无关性,针对不同系统有特定实现(Windows,Linux,macOS)
java程序从源代码到运行:
1、Java 程序 先经过JDK中的javac 编译,生成字节码.class
文件;
2、在通过类加载器把字节码文件加载进JVM虚拟机内存,然后通过解释器逐行解释执行;
3、最后转化成机器可执行的二进制机器码。
==比较运算符 和 equals()方法
== 比较运算符:
==== 比较运算符,返回的是一个布尔值 true false
基本数据类型:比较的是内容值
引用数据类型:比较的是两个对象的地址值==
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,
其本质比较的都是值,只是引用类型变量存的值是对象的地址。
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
equals()方法:
//Object类是所有类的直接或间接父类。
//Object 类 equals() 方法:
//equals方法源码:
//boolean equals(Object obj) 指示其他某个对象是否与此对象“相等”。
public boolean equals(Object obj) {
return (this == obj); //也是用 == 进行比较的
}
Object类的equals方法,默认比较的是两个对象的地址值,没有意义。所以一般使用的时候要重写equals方法。(相等条件自己定义。比如:年龄相等就认为这两个类相等。)
- 自己写的People类
问题:
隐含着一个多态 //java.lang.Object
类是Java语言中的根类,即所有类的父类。
多态的弊端: 无法使用子类特有的内容(属性和方法)
解决: 可以使用向下转型(强转)把obj类型转换为Person
//手动覆盖重写自己想要实现的 【比较两个对象的属性(例如:name,age)】
@Override
public boolean equals(Object obj) {
//增加一个判断,传递的参数obj如果是this本身,直接返回true,提高程序的效率
if(obj==this){
return true;
}
//增加一个判断,传递的参数obj如果是null,直接返回false,提高程序的效率
if(obj==null){
return false;
}
//增加一个判断,防止类型转换异常ClassCastException
/*
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
*/
if(obj instanceof Person){
//`java.lang.Object`类是Java语言中的根类,即所有类的父类。
//使用向下转型,把obj转换为Person类型
Person p = (Person)obj;
//比较两个对象的属性,一个对象是this(p1),一个对象是p(obj->p2)
//如果name和age的值都相同,在计算hashCode()时,返回相同的哈希值。
boolean b = this.name.equals(p.name) && this.age==p.age;
return b;
}
//不是Person类型直接返回false
return false;
}
//Generate-equals自动生成
@Override
public boolean equals(Object o) {
//如果传递的参数o如果是this本身,直接返回true,提高程序的效率
if (this == o) return true;
//判断传递的参数obj是否为null;判断o是否是Person类型
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
//如果name和age的值都相同,在计算hashCode()时,返回相同的哈希值。
return age == person.age && Objects.equals(name, person.name);
}
//getClass() != o.getClass() 使用反射技术,判断o是否是Person类型。等效于obj instanceof Person
//重写equals()方法事必须重写hashcode()方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
- String.java
String x= "abc";
String y = "ab";
System.out.println(x.equals(y)); //false,覆盖重写后,比较的是 对象的值
/*
String 中的 equals 方法是被重写过的,
因为 对于引用类型String,比较的是对象的地址值
覆盖重写String的equals方法后 比较的是对象的值。
*/
//查看其源码
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (!COMPACT_STRINGS || this.coder == aString.coder) {
return StringLatin1.equals(value, aString.value);
}
}
return false;
}
- Integer.java
Integer i1 = 12;
Integer i2 = 12;
System.out.println(i1.equals(i2)); //true
//查看源码
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
为什么重写 equals
时必须重写 hashCode
方法
0、Hash:
哈希:就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值。(压缩映射)
1、hashCode()介绍:
hashCode()
的作用是获取 哈希码(散列码);它实际上是返回一个32位 int 整数。
这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()
定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode()
函数。
Object 的 hashcode()
方法是本地方法,也就是用 c 或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。
public native int hashCode();
2、为什么要有 hashCode?
我们以“HashMap集合
如何检查重复”为例子来说明为什么要有 hashCode?
1、put添加一个元素时,先计算元素的哈希值( hashcode),再把哈希值转成—>table的索引,来确定元素加入的位置。
2、判断该索引位置是否已经存放元素。如果没有,直接加入。
3、如果有(hash 冲突),调用equals比较,该元素的key和准备加入的key是否相同(如重写equals,按自己定义的规则比较)
相同,就替换(相同的key替换,相当于不变,value替换为新值)。
如果不相同需要判断是树结构还是链表结构,然后再添加。添加时发现容量不够则需要扩容。
…
如果hashCode不相同,就会认为没有重复的对象。
发现有相同 hashcode 值的对象时,会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。
如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
如果哈希值相同(出现哈希冲突),才会调用equals()方法来检查hashcode相等的对象是否真的相同。
这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
3、为什么重写 equals
时必须重写 hashCode
方法?
如果两个对象相等,则 hashcode 一定相同。但两个对象有相同的 hashcode 值,它们却不一定是相等的 。
因此,equals 方法被覆盖过,则 hashCode
方法也必须被覆盖。
如果原先两个不同对象的哈希值 是不相等的。覆盖equals 方法使得他们相等了。如果不重写
hashCode
方法,那么他们的哈希值 一直是不相等的。不符合前面说的。
hashCode()
的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()
,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
4、为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?
因为 hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode
。
我们刚刚也提到了 HashSet
,如果 HashSet
在对比的时候,同样的 hashcode 有多个对象,它会使用 equals()
来判断是否真的相同。也就是说 hashcode
只是用来缩小查找成本。
内存
JAVA的JVM的内存可分为3个区:堆(heap)、堆栈(stack)和方法区(method)
堆区:
1、提供所有类实例和数组对象存储区域
2、jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区:
1、每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2、每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
方法区:
1、方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2、方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
3、运行时常量池都分配在 Java 虚拟机的方法区之中
String 类和常量池
字符型常量和 字符串常量的区别
-
形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符
-
含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)
-
占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节),
字符封装类
Character
有一个成员常量Character.SIZE
值为 16,单位是bits
,该值除以 8(1byte=8bits
)后就可以得到 2 个字节
String、StringBuffer、StringBuilder区别
- String 跟其他两个类的区别是
String是final类型,每次声明的都是不可变的对象,
所以每次操作都会产生新的String对象,然后将指针指向新的String对象。
- StringBuffer,StringBuilder都是在原有对象上进行操作
所以,如果需要经常改变字符串内容,则建议采用这两者。
- StringBuffer vs StringBuilder
前者是线程安全的,后者是线程不安全的。
线程不安全性能更高,所以在开发中,优先采用StringBuilder.
StringBuilder > StringBuffer > String
String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String 对象的生成。
StringBuffer:跟 String 类似,但是值可以被修改,使用 synchronized 来保证线程安全。
StringBuilder:StringBuffer 的非线程安全版本,没有使用 synchronized,具有更高的性能,推荐优先使用。
1、可变性
String
是 final 关键字修饰 字节数组( Java 9 之后) 来保存字符串的,private final byte[] value[]
,所以String
对象是不可变的。String
类无被final关键字修饰,该类法被继承。public final class String
而 StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用 字节数组 保存字符串byte[] value
但是没有用 final
关键字修饰,所以这两种对象都是可变的。
在 Java 9 之后,
String
、StringBuilder
与StringBuffer
的实现改用 byte 字节数组存储字符串
2、线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
3、性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。
StringBuilder
线程不安全性能更高。
StringBuilder > StringBuffer > String
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
String 对象的两种创建方式
String str1 = "abcd";
//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后 str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"";
String str2 = new String("abcd"); //堆中创建一个新的对象
String str3 = new String("abcd"); //堆中创建一个新的对象
System.out.println(str1==str2); //false
System.out.println(str2==str3); //false
str1、str2 两个语句都会先去字符串常量池中检查是否已经存在 “abcd”,如果有则直接使用,如果没有则会在常量池中创建 “abcd”对象。
另外,String str2 = new String(“abcd”); 还会通过 new String() 在堆里创建一个内容与 “abcd” 相同的对象实例。
所以前者其实理解为被后者的所包含。
这两种不同的创建方法是有差别的。
- 第一种方式是在常量池中拿对象;
- 第二种方式是直接在堆内存空间创建一个新的对象。
记住一点:一旦new,就是在堆中开辟了一块新内存
String 类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的 String 对象会直接存储在常量池中。
- 如果不是用双引号声明的 String 对象,可以使用 String 提供的
intern()
方法。String.intern()
是一个 Native 方法,它的作用是:如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则返回常量池中该字符串的引用;如果没有,JDK1.7 之前(不包含 1.7)的处理方式是在常量池中创建与此 String 内容相同的字符串,并返回常量池中创建的字符串的引用,JDK1.7 以及之后的处理方式是在常量池中记录此字符串的引用,并返回该引用。
JDK8 :
String s1 = "计算机";
String s2 = s1.intern(); //.intern();常量池中创建
String s3 = "计算机";
System.out.println(s2); //计算机
System.out.println(s1 == s2); //true
System.out.println(s3 == s2); //true,因为两个都是常量池中的 String 对象
字符串拼接
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing"; //常量池中的对象
String str4 = str1 + str2; //在堆上创建的新的对象
String str5 = "string"; //常量池中的对象
System.out.println(str3 == str4); //false
System.out.println(str3 == str5); //true
System.out.println(str4 == str5); //false
尽量避免多个字符串拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
String s1 = new String(“abc”)创建了几个字符串对象?
将创建 1 或 2 个字符串。
如果池中已存在字符串常量“abc”,则只会在堆空间创建一个字符串常量“abc”。
如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
验证:
String s1 = new String("abc"); // 堆内存的地址值
String s2 = "abc";
System.out.println(s1 == s2); // false, 因为一个是堆内存,一个是常量池的内存,故两者是不同的。
System.out.println(s1.equals(s2));// true
问题1 :这儿有问题吧 ?字符串equals是覆盖重写了的
问题2
String Pool
(字符串池),即String Literal Pool
, 又叫全局字符串池
、字符串常量池
。是在类加载完成,经过验证,准备阶段之后 在 堆 中生成字符串对象实例,然后 将该字符串对象实例的 引用值 存到 String Pool 中。
记住:String Pool 中存的是 引用值,而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
?????是不是冲突了?????
8 种基本类型的包装类和常量池
基本类型:
整数 (byte,short,int,long)
浮点数(float,double)
字符 (char)
布尔 (boolean)
引用类型
数组,类,接口 ,字符串…除了8种基本类型外,其余都是引用类型
引用类型在内存中保存的是地址值
类型转换
自动转换:byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double
强制转换:数据类型 变量名 = (数据类型)被转数据值;
Java 基本类型的包装类的大部分都实现了常量池技术: (浮点数(float,double) 没有)
-
Byte、Short、Integer、Long 这4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,
-
Character、 创建了数值在[0,127]范围的缓存数据
-
Boolean 直接返回 True Or False。
如果超出对应范围就会去创建新的对象。
为啥把缓存设置为[-128,127]区间?(参见 issue/461)性能和资源之间的权衡。
//Boolean
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
//Character
private static class CharacterCache {
private CharacterCache(){}
static final Character cache[] = new Character[127 + 1];
static {
for (int i = 0; i < cache.length; i++)
cache[i] = new Character((char)i);
}
}
- 两种浮点数类型的包装类 Float、Double 并没有实现常量池技术。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2); // true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// false,超出缓存,new
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// false,没有实现常量池技术,直接new
Integer 缓存源代码: [-128 到 127]
//此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
应用场景:
- Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
- Integer i1 = new Integer(40);这种情况下会创建新的对象。
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出 falseCopy to clipboardErrorCopied
Integer 比较更丰富的一个例子:
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
System.out.println("i1=i2 " + (i1 == i2)); //true
System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); //true,常量池中
System.out.println("i1=i4 " + (i1 == i4)); //false
System.out.println("i4=i5 " + (i4 == i5)); //false
/*
语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。
*/
System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); //true
System.out.println("40=i5+i6 " + (40 == i5 + i6)); //true
Int和Integer的区别
- 包装类
* 装箱:从基本类型转换为对应的包装类对象。
* 拆箱:从包装类对象转换为对应的基本类型。
用Integer与 int为例:
//基本数值---->包装对象
Integer i = new Integer(4); //使用构造函数函数
Integer iii = Integer.valueOf(4); //使用包装类中的valueOf方法
//包装对象---->基本数值
int num = i.intValue();
装箱 调用了 包装类的valueOf()方法,
拆箱 调用了 xxxValue()方法。
从Java 5(JDK 1.5)开始,基本类型与包装类的装箱、拆箱动作可以自动完成。
Integer i = 4; //自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5; //等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。
- 为什么要包装类
1、让基本类型有了对象的性质,丰富基本类型的操作。
2、基本类型存储在 栈的局部变量表中,List,Set,Map
集合等容器类装的是object对象 这就需要把基本类型转为包装类了。
- 都定义为Integer的比较:
//一旦new,就是在堆中开辟了一块新内存,不相等
//new的在堆内存中,new几个堆内存中就保存几个
Integer i1 = new Integer(12);
Integer i2 = new Integer(12);
System.out.println(i1 == i2); //false
//==对于引用类型比较的是地址值
//对于引用类型,一般用equals()进行比较
//不new,看范围
//int 包装类Integer 实现了 **常量池技术**
Integer做了缓存,-128至127,当你取值在这个范围的时候,会采用缓存的对象,所以会相等
当不在这个范围,内部创建新的对象,此时不相等
Integer i3 = 126;
Integer i4 = 126;
System.out.println(i3 == i4); //true
Integer i5 = 128;
Integer i6 = 128;
System.out.println(i5 == i6); //false
//当我们写Integer i = 126,实际上做了自动装箱:Integer i = Integer.valueOf(126);
//看源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//IntegerCache是Integer的内部类
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
- Integer和int的比较:
实际比较的是数值,Integer会做拆箱的动作,来跟基本数据类型做比较
此时跟是否在缓存范围内或是否new都没关系
Integer i71 = 126;
Integer i72 = 126;
Integer i81 = 128;
Integer i82 = 128;
int i9 = 126;
Integer i10 = new Integer(126);
System.out.println(i71 == i72 == i9 == i10);
System.out.println(i9 == i10);
System.out.println(i81 == i82);
栈
栈的特点是:FILO(First In Last Out)
public class Test {
public static void main(String[] args) {
Stack s = new Stack();
s.push(1);
s.push(2);
s.push(3);
Object x = s.peek();
System.out.println(x);
}
}
Debug就好。
存:
底层是数组实现
取:
获取数组长度,根据数组索引,取出栈顶元素
泛型
(1)、泛型T
由于集合中什么类型的元素都可以存储,导致取出时强转引发ClassCastException类型转换异常。
Collection虽然可以存储各种对象,但实际上通常Collection只存储同一类型对象。不然:不安全,会引发异常
Collection coll = new ArrayList();
coll.add("abc");
coll.add(5); //由于集合没有做任何限定,任何类型都可以给其中存放
Iterator it = coll.iterator();
//迭代,打印字符串长度
while(it.hasNext()){
String str = (String) it.next(); //运行时,5 强转成String类型报错
System.out.println(str.length());
}
泛型:可以在 类、方法、接口中预支地使用未知的类型。
Collection<String> list = new ArrayList<String>();
list.add("abc");
// list.add(5); //集合明确类型后,存放类型不一致就会 编译报错
java编译器是通过 先检查代码中泛型的类型,然后进行类型擦除,再进行编译的。
泛型好处:
将运行时期的 类型转换异常ClassCastException,转移到了编译时期变成了编译失败。
避免了类型强转的麻烦。安全
弊端:
泛型是什么类型,只能存储什么类型的数据
泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递
(泛型的本质是参数化类型)
1、含有泛型的类(也可以自定义泛型类)
class ArrayList<E>{....//API中的ArrayList集合
//在创建对象的时候确定泛型
ArrayList<String> list = new ArrayList<String>();
2、含有泛型的方法
public <T> void show(T t) {
System.out.println(t.getClass());
}
//调用方法时,确定泛型的类型
mm.show("aaa");
mm.show(123);
3、含有泛型的接口
public interface MyGenericInterface<E>{
//定义类时确定泛型的类型
public class MyImp1 implements MyGenericInterface<String> {....
//也可以:始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class MyImp2 implements MyGenericInterface<E> {....
MyImp2<String> my = new MyImp2<String>();
(2)、泛型擦除
Java 的泛型是伪泛型,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除。
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());//true
//泛型信息被擦除了。
//泛型类和普通类在 java 虚拟机内是没有什么特别的地方。
//List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。
java编译器是通过 先检查代码中泛型的类型,然后进行类型擦除,再进行编译的。
Collection<String> list = new ArrayList<String>();
list.add("abc");
// list.add(5); //集合明确类型后,存放类型不一致就会 编译报错
//通过反射可以添加成功
list.getClass().getMethod("add", Object.class).invoke(list, 5);
1、虚拟机使用 桥方法,来解决 类型擦除和多态的冲突。
2、泛型类型变量不能是基本数据类型。
3、泛型类中的静态方法和静态变量不可以使用 泛型类所声明的泛型类型参数
public class Test2<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
/*创建对象的时候才确定泛型,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
但是要注意区分下面的一种情况:*/
public class Test2<T> {
public static <T>T show(T one){ //这是正确的
return null;
}
}
/1、
public class Car <T>{
T object;
public Car(T object) {
this.object = object;
}
}
//泛型类使用时确定泛型
Car<String> car = new Car<String>("hello");
Class carClass = car.getClass(); //获取class对象
System.out.println(carClass.getName()); //获取他的全类名
//com.cn.test.Car
//Class 的类型仍然是 Car 并不是 Car<T>这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。
Field[] fs = carClass.getDeclaredFields(); //获取所有的成员变量
for ( Field f:fs) {
System.out.println(f.getName()); //object
System.out.println(f.getType().getName()); //java.lang.Object
}
/2、
public class Erasure <T extends String>{
//同理....
`<T>`则会被转译成普通的 Object 类型,如果指定了上限如 `<T extends String>`则类型参数就被替换成类型上限Sting。
(3)、通配符—— ?
通配符代表的是一种未知的类型,常用在方法上。
已经有了 的形式了,为什么还要引进 <?>这样的概念呢?
泛型中参数化类型不会考虑继承关系
ArrayList<String> arrayList1=new ArrayList<Object>();//编译错误
/*
ArrayList<Object> arrayList1=new ArrayList<Object>();
arrayList1.add(new Object());
arrayList1.add(new Object());
ArrayList<String> arrayList2=arrayList1;//编译错误 */
ArrayList<Object> arrayList1=new ArrayList<String>();//编译错误
/*
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add(new String());
arrayList1.add(new String());
ArrayList<Object> arrayList2=arrayList1;//编译错误 */
//虽然Object和String有继承关系,但ArrayList<Object>和ArrayList<String>是没有继承的
为满足实际需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,Java 引入了 通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
泛型: 是一个形参,可以理解为一个占位符,被使用时,会在程序运行的时候替换成具体的类型。
比如泛型类Demo<T>
,在创建对象时被替换成具体的类型。new Demo<Integer>(123);
**通配符:**是一个实参,这是Java定义的一种特殊类型,比Object更特殊。就像Demo<?> obj
,?代表一种未知的类型,他可以充当任何一个角色(类)。更似于是java中所有对象的的父类。
总结:通俗的讲,泛型可以由我们指定为某一类对象,而通配符类则是所有对象都通用。
public class Test {
public static void main(String[] args) {
//Number是Integer的父类
Demo<Integer> gInteger = new Demo<Integer>(123);
Demo<Number> gNumber = new Demo<Number>(456);
showKeyValue1(gNumber);
showKeyValue1(gInteger); //编译不通过
//Demo<Integer>不能被看作为Demo<Number>的子类。
showKeyValue2(gNumber);
showKeyValue2(gInteger);
}
private static void showKeyValue1(Demo<Number> obj) { //Demo<Number>类型的obj
System.out.println(obj.getKey());
}
private static void showKeyValue2(Demo<?> obj) { //?和Number一样,都是一种实际的类型
System.out.println(obj.getKey());
}
//如果这儿使用Object,用的时候就需要强转。
}
//泛型类
class Demo<T>{
private T key;
public Demo(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
通配符有 3 种形式:
<?>被称作无限定的通配符。
<? extends T> 有上限的通配符。 //只能接收该类型及其子类
<? super T> 有下限的通配符。 //只能接收该类型及其父类型
无限定通配符经常与容器类配合使用。
一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。此时只能接受数据,不能往该集合中存储数据。(Object类中没有add方法)(<?>
被转 擦除译成了普通的 Object 类型)
public void testWildCards(Collection<?> col){
col.add(123); // 编译不通过
col.add("hello"); // 编译不通过
}
Collection<?> col = new ArrayList<>();
col.add(123); // 编译不通过
(4)、泛型【T】与通配符【?】的区别
Java编译器会把泛型【T】推断成具体类型,而把通配符【?】推断成未知类型。
Java编辑器只能操作具体类型,不能操作未知类型,如果有对参数做修改的操作就必须要使用泛型,如果仅仅是查看就可以使用通配符。
Java 不能创建具体类型的泛型数组
List<Integer>[] list1 = new ArrayList<Integer>[]; // 编译不通过
List<Boolean> list2 = new ArrayList<Boolean>[]; // 编译不通过
//List<Integer>和 List<Boolean>在 jvm 中等同于List<Object>,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean>类型。
但是通配符可以
List<?>[] list3 = new ArrayList<?>[10];
list3[1] = new ArrayList<String>();
List<?> temp = list3[1];
/*
借助于无限定通配符却可以, ?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。
比如,上面的 temp 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作。
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
异常
异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处
理异常的方式是中断处理。
(1)、异常类层次结构
Error
:Error
属于程序无法处理的错误 ,我们没办法通过catch
来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
Exception
:程序本身可以处理的异常。Exception
又可以分为 编译时期异常(checked异常),在编译时期就会检查,必须处理异常,否则编译失败。(如日期格式化异常) 。运行时期异常(runtime异常),在运行时期检查异常。,可以不处理。在编译时期,运行异常不会编译器检测(不报错)。(NullPointerException
、NumberFormatException
(字符串转换为数字)、ArrayIndexOutOfBoundsException
(数组越界)、ClassCastException
(类型转换错误)、ArithmeticException
(算术错误)等。
(2)、Throwable 类常用方法
public void printStackTrace() //打印异常的详细信息
public string getMessage() //返回异常发生时的简要描述
public string toString() //返回异常发生时的详细信息
(3)异、常的处理
Java异常处理的五个关键字:try**、catch、fifinally、throw、**throws
抛出异常throw
throw
用在方法内,用来抛出一个指定的异常对象。将这个异常对象返回给该方法调用者,并结束当前方法的执行。
声明异常throws
将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理,那么必须通过throws进行声明,让调用者去处理。
throws用于方法声明之上,表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。
* throw抛出异常,throws声明异常给方法调用者,方法调用者又声明异常交给JVM处理(中断处理)
public class ThrowsDemo {
//FileNotFoundException extends IOException extends Exception
//如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可
public static void main(String[] args) throws IOException {
read("a.txt");
System.out.println("后续代码"); //不会执行,因为交给了虚拟机中断处理
}
public static void read(String path)throws FileNotFoundException, IOException {
if (!path.equals("a.txt")) {
// 我假设如果不是 a.txt,就认为该文件不存在是一个异常
throw new FileNotFoundException("文件不存在");
}
if (!path.equals("b.txt")) {
throw new IOException();
}
}
}
捕获异常——try…catch
如果异常出现的话,会立刻终止程序,所以我们得处理异常:
-
该方法不处理,而是声明抛出,由该方法的调用者来处理(throws)。
-
在方法中使用
try-catch
的语句块自己来处理异常。
* throw抛出异常后,要么throws声明异常给方法调用者 要么 方法调用者用try catch捕获处理异常
public class TryCatchDemo {
public static void main(String[] args) {
// 当产生异常时,必须有处理方式。要么捕获,要么声明。
try {
//可能会出现异常的代码
read("b.txt");
} catch (FileNotFoundException e) {
//处理异常的代码 // 记录日志/打印异常信息/继续抛出异常
System.out.println(e);
}
System.out.println("over"); //会被执行。try catch 后继续执行以后代码。
}
public static void read(String path) throws FileNotFoundException {
if (!path.equals("a.txt")) {
// 我假设如果不是 a.txt,就认为该文件不存在是一个异常
throw new FileNotFoundException("文件不存在");
}
}
}
finally 代码块 :try…catch…finally
finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行 不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
【注意:finally不能单独使用。try…catch…finally】
例如:
当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都会在使用完之后,最终关闭打开的资源。
try{
read("a.txt");
System.out.println("资源释放"); //如果read产生异常,此就会跳转到catch中处理异常,就不会执行"资源释放"输出。
}catch .....
try {
read("a.txt");
} catch (FileNotFoundException e) {
//抓取到的是编译期异常 抛出去的是运行期。
throw new RuntimeException(e);
} finally {
System.out.println("不管程序怎样,这里都将会被执行。");
}
System.out.println("over"); //会被执行。try catch 后继续执行以后代码。
finally
块不会被执行的情况:
- 在
try
或finally
块中用了System.exit(int)
退出程序。但是,如果System.exit(int)
在异常语句之后,finally
还是会被执行。 - 程序所在的线程死亡。
- 关闭 CPU。
子父类异常
- 如果父类抛出了多个异常,子类重写父类方法时,抛出 和父类相同的异常 或者是父类异常的子类 或者不抛出异
常。
- 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不
能声明抛出
面试题
public int getNum() {
try {
int a = 1 / 0;
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
//代码到第3行的时候遇到了一个MathException,这时第4行的代码就不会执行了,代码直接跳转到catch语句中,
//到第 6 行的时候,异常机制有一个原则:
如果在catch中遇到了return或者 异常等能使该函数终止的话,那么有finally就必须先执行完finally代码块里面的代码然后再返回值。
//因此代码又跳到第8行,可惜第8行是一个return语句,那么这个时候方法就结束了,因此第6行的返回结果就无法被真正返回。
//如果finally仅仅是处理了一个释放资源的操作,而不是return或者 异常等能使该函数终止的话,那么该道题最终返回的结果就是2。因此上面返回值是3。
文件和I/O流
什么是流
任何一个文件都是以二进制形式存在于设备中,计算机就只有 0 和 1,你能看见的东西全部都是由这两个数字组成,你看这篇文章时,这篇文章也是由01组成,只不过这些二进制串经过各种转换演变成一个个文字、一张张图片跃然屏幕上。
而流就是将这些二进制串在各种设备之间进行传输。
下图是一张图片,它由01串组成,我们可以通过程序把一张图片拷贝到一个文件夹中,
把图片转化成二进制数据集,把数据一点一点地传递到文件夹中 , 类似于水的流动 , 这样整体的数据就是一个数据流
序列化 和 反序列化
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
- 序列化: 将数据结构或对象 转换成二进制字节流(也就是byte[]数组)的过程
- 反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们 序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
综上:序列化的主要目的是 通过网络传输对象 或者说是 将对象存储到文件系统、数据库、内存中。
serialVersionUID的作用是什么:
当执行序列化时,我们写对象到磁盘中,会根据当前这个类的结构生成一个版本号ID
当反序列化时,程序会比较磁盘中的序列化版本号ID跟当前的类结构生成的版本号ID是否一致,如果一致则反序列化成功,否则,反序列化失败
加上版本号,有助于当我们的类结构发生了变化,依然可以之前已经序列化的对象反序列化成功
Java 序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
transient
只能修饰变量,不能修饰类和方法。
//transient表示瞬间,短暂的,表示该属性不会被序列号
获取用键盘输入常用的两种方法:
方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
IO 流分类:
- 根据数据的 流向 分为:输入流、输出流。
输入流 :把数据从 其他设备 上读取到 内存 中的流。
输出流 :把数据从 内存 中写出到 其他设备 上的流。
- 按照操作 单元划分:字节流,字符流;
- 按照流的 角色划分 :节点流,处理流。
IO流的4大基类 | 输入流 | 输出流 |
---|---|---|
字节流 | 字节输入流 InputStream | 字节输出流 OutputStream |
字符流 | 字符输入流 Reader | 字符输出流 Writer |
既然有了字节流,为什么还要字符流?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
BIO、NIO、AIO有什么区别?
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。
当应用程序发起 I/O 调用请求后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
【应用程序发起 I/O 调用请求 --> 内核等待 I/O 设备准备好数据 -->数据从内核空间拷贝到用户空间】
常见的 IO 模型
UNIX 系统下, 5 种 IO 模型: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和 异步 I/O。
同步、异步描述的是:客户端在请求数据的过程中,能否做其他事情。
阻塞、非阻塞描述的是:客户端与服务端是否从头到尾始终都有一个持续连接,以至于占用了通道,不让其他客户端成功连接。
(1)、BIO (Blocking I/O)
BIO 属于 同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
(1)、NIO (Non-blocking/New I/O)]
NIO 对应于 java.nio
包,提供了 Channel
, Selector
,Buffer
等抽象。
NIO三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
通道(Channel)
Channel 表示一个连接,可以理解为每一个请求。所有的通道都会注册到统一个选择器(Selector)上实现管理,在通过选择器将数据统一写入到 buffer中。
缓冲区(Buffer)
Buffer本质上就是一块内存区,可以用来读取数据,也就先将数据写入到缓冲区中、在统一的写入到硬盘上。
选择器(Selector)
Selector可以称做为选择器,也可以把它叫做多路复用器,可以在单线程的情况下可以去维护多个Channel,也可以去维护多个连接;
NIO 中的 N在操作系统中可以理解为 Non-blocking(java中,new IO)。它是面向缓冲的,基于通道的 I/O 操作方法。
对于高负载、高并发的(网络)应用,应使用 NIO 。
Java 中的 NIO 可以看作是 I/O 多路复用模型(我们认为)。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
- 同步非阻塞 IO 模型。
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
- 这个时候,I/O 多路复用模型 就上场了。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用,如此这个过程就不是阻塞的。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。
目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,是目前几乎在所有的操作系统上都有支持。但是性能低下,后来渐渐演化成了Linux下的epoll和Mac里的kqueue。
- select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
- epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
IO 多路复用模型,通过减少 无效的系统调用,减少了对 CPU 资源的消耗。
- 多路复用器
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
NIO三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
(3)、AIO (Asynchronous I/O)
AIO是异步 IO 模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。
(4)、总结
Java 中的 BIO、NIO、AIO。
select、poll、epoll区别
**select**
(阻塞函数)其实就是将对FD(标志位)数据的集合判断从原来的用户态到交给内核态管理(将Rset从用户态拷贝到了内核态),内核态会快很多,如果判断有数据
- 将FD置位,表示此时有数据来了
- 将select返回,不再阻塞
之后判断那一个FD被置位了,于是就行取数据处理
select使用的1024的bitmap存数据
缺点:
- fd_size有限制1024bitmap
- FDset不可重用
- 用户态到内核态的拷贝开销
- O(n)再次遍历
poll
**poll**
(阻塞函数),工作原理与select很相似, 但是poll没有采用bitmap,采用的pollfd存储(基于结构体存储fd)
struct pollfd {
int fd; short events; short revents;
}
有数据:
- pollfd.revents置位
- poll返回,不再阻塞
执行之后的操作,执行之后再将pollfd.revents设置为0
缺点:
- 用户态到内核态的拷贝开销
- O(n)再次遍历
epoll
**epoll**
,最新的一种IO多路复用的函数,用户态和内核态共享epfd数组,内核用于判断哪个fd有数据到来,不需要用户态到内核态的拷贝,不需要轮询,时间负责度O(1)
有数据:
- 置位,通过重排置位,将数组中有数据的fd放在前面的位置
- 返回,有返回值,返回一共有多少个fd触发了事件,这样可以实现遍历复杂度为O(1)
Redis、Ngnix都是使用的epoll,JavaNIO在Linux系统下也是采用的epool实现的
反射
//反射:框架设计的灵魂
* 框架:半成品软件。可以在框架的基础上进行软件开发,简化编码
* 反射:将类的各个组成部分封装为其他对象,这就是反射机制
* 好处:
1. 可以在程序运行过程中,操作这些对象。(比如说写程序的时候String.什么,有提示)
2. 可以解耦,提高程序的可扩展性。
JAVA反射机制是:在运行状态中,
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意方法和属性;
这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
- 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。Java Reflection: Why is it so slow?
反射: 透过这个镜子看到类的结构。
//获取Class对象的三种方式
1、Class.forName("全类名")
2、类名.class
3、对象.getClass()
4、类加载器 ClassLoader.loadClass("全类名");
通过类加载器获取 Class 对象不会进行初始化,意味着不进行 初始化等一些列步骤,静态块和静态对象不会得到执行
//1、获取成员变量们
Field[] getFields() 获取所有public修饰的成员变量
Field getField(String name) 获取指定名称的 public修饰的成员变量
Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
Field getDeclaredField(String name) 获取指定名称成员变量,不考虑修饰符
void set(Object obj, Object value) 设置值
get(Object obj) 获取值
equals(Object obj) 属性与obj相等则返回true
setAccessible(true) //暴力反射。忽略访问权限修饰符的安全检查
//2、获取构造方法们
Constructor<?>[] getConstructors()
Constructor<T> getConstructor(类<?>... parameterTypes)
Constructor<?>[] getDeclaredConstructors()
Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
创建对象:
T newInstance(Object... initargs) //传参
如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
//3、获取成员方法们:
Method[] getMethods()
Method getMethod(String name, 类<?>... parameterTypes)
Method[] getDeclaredMethods()
Method getDeclaredMethod(String name, 类<?>... parameterTypes)
Object invoke(Object obj, Object... args) 执行方法
String getName 获取方法名称
//4、获取全类名
String getName()
应用场景:
1、动态代理——
Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
2、注解的实现
比如 使用 Spring 的时候 ,一个@Component
注解就声明了一个类为 Spring Bean ;一个 @Value
注解就读取到配置文件中的值
可以基于反射分析类,然后获取到 类/属性/方法/方法的参数 上的注解。获取到注解之后,就可以做进一步的处理。