文章目录
一. Java概述
1. JVM、JRE 和 JDK
JVM
Java Virtual Machine是Java虚拟机。Java程序需要在虚拟机上运行,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
JRE
Java Runtime Environment包括Java虚拟机和Java程序所需要的核心类库等。核心库主要是java.lang 包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包。如果想要运行一个开发好的程序,计算机中只需要安装JRE即可。
JDK
Java Development Kit 是提供给Java开发人员使用的,其中包含了Java的开发工具,也包含了JRE。其中的开发工具:运行工具(java.exe),编译工具(javac.exe),打包工具(jar.exe)等。
2. 什么是Java的跨平台性?原理是什么?
所谓的跨平台,即就是Java语言编写好的程序,一经编译后,可以在多个系统平台上运行。
实现原理:Java程序是通过Java虚拟机在系统平台上运行的,不同的系统平台有自己的虚拟机,只要该系统可以安装相应的Java虚拟机,该系统就可以运行Java程序。
3. 什么是字节码?采用字节码的最大好处
字节码:Java源代码经过虚拟机的编译器编译后产生的文件(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
采用字节码的好处:Java语言通过字节码的方式,在一定程度上解决了传统解释性语言执行效率低的问题,同时又保留了解释性语言可移动的特点。所以Java程序运行时比较高效,且Java语言具有可移植性,因此Java语言无需重新编译即可在多种不同的计算机上运行。
Java语言的执行过程
Java源代码 —— 编译器 —— jvm可执行的Java字节码 —— jvm —— jvm的解释器中 —— 机器可执行的二进制机器码 —— 程序运行
Java程序的主类?应用程序和小程序的主类有何不同?
主类是Java程序执行的入口点,一个程序中可以有多个类,但是只能有一个主类,且类前用public修饰。
在Java的应用程序中,这个主类是指包含 main() 方法的类。Java应用程序从主线程启动,即 main() 方法。
而在Java小程序中,这个主类是一个继承自系统类JApplet或Applet的子类。Java小程序是嵌在浏览器页面上运行(调用init()线程或者run() 来启动),嵌入浏览器这点跟flash的小游戏类似。
二. 基础语法
1. 数据类型
Java的数据类型
分类
- 基本数据类型
- 数值型
- 整数类型(byte, short, int, long)
- 浮点类型(float, double)
- 字符型(char)
- 布尔类型(boolean)
- 数值型
- 引用数据类型
- 类(class)
- 接口(interface)
- 数组([])
- 字符串(String)
数据作为成员变量时可以不进行初始化,系统会赋予默认值,但是数据若是作为方法或者其他的局部变量必须赋予初始值。
还有整型变量默认为 int 类型,浮点数变量的默认类型为 double 类型。
switch 语句的判断条件
从Java5 开始,Java的 switch(expr) 中,expr 可以是 enum(枚举)、byte、short、int、char、String。
左移、右移
左移相当于乘<<,例如 2 << 3,2左移3位相当于2 * (2的三次方)。
右移相当于除>>,例如 8 >> 3,8右移3相当于8 / (三次根号下8),即为2。
取两个数的平均数
int a = 7;
int b = 17;
int avg = ?;
- avg = (a + b) / 2;
- avg = a + (b - a) / 2;
- avg = (a + b) >> 1;
求一个数的四舍五入数
Math.round(11.5) —— 返回12;
Math.round(-11.5) —— 返回-11.
四舍五入的原理是在参数上 + 0.5 然后向下取整
2. 访问修饰符
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
private | 可以 | 不可 | 不可 | 不可 |
default | 可以 | 可以 | 不可 | 不可 |
protected | 可以 | 可以 | 可以 | 不可 |
public | 可以 | 可以 | 可以 | 可以 |
上表中体现的是该修饰符对于哪些类是可见的。
3. 运算符
& 和 && 的区别
& 运算符的用法有两个:(1)按位与;(2)逻辑与
&& 运算符的用法:短路与。
虽然两者都要求运算符左右两边的布尔值都为true整个表达式的判断结果才是true,但他们的区别还是很大的。&&之所以被称为是短路与,是因为如果 && 左边的表达式为 false,右边的表达式会被直接短路掉,右边的表达式不会进行运算。同理于 | 和 ||
4. 几个关键字
final 关键字作用
用于修饰类、属性和方法
- 被 final 修饰的类不可以被继承
- 被 final 修饰的方法不可以被重写
- 被 final 修饰的变量不可以被改变,被 final 修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以变的。
final finally finalize的区别
- final 同上个知识点
- finally 一般作用在 try - catch 代码块中,在处理异常的时候,通常将一些必须执行的代码放在 finally 代码块中。
- finalize 是一个方法,属于 Object 类的一个方法,而Object 类是所有类的方法,该方法一般由垃圾回收器来调用。当我们调用 System.gc() 的时候,由垃圾回收器调用 finalize(),回收垃圾。
this 关键字的使用
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法:
- 普通的直接调用,this 相当于指向当前对象本身。可以调用属性、普通方法。
- 形参与成员名重名,用this调用成员名来区分。
- 引用本类的构造函数。可用于构造函数给成员变量逐个赋值的时候。
super 关键字的使用
super 是指向自己父类对象的一个指针(离自己最近的一个父类)。
super 的用法:
- 普通的直接引用,指向当前对象的父类的引用,这样就可以用 super.xxx 来引用父类的成员。
- 子类中的成员变量或方法与父类中的成员变量或方法同名时,用 super 调用来区分。
- 引用父类的构造函数
- super(参数):调用父类中的另一个构造函数(应该为构造函数中的第一句)
- this(参数):调用本类中另一种形式的构造函数(应该为本构造函数的第一句)
this 和 super 的区别
- super:引用当前对象的直接父类中的成员;super是在子类中调用父类的属性、构造方法、普通方法;
- this:代表当前对象名;this是在本类中调用本类的属性 、构造方法、方法;
- super() 和 this() 均需放在构造方法的第一行;
- this 和 super 不可以同时出现在一个构造函数中,因为this会调用其他的构造函数,其他的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
- this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用(static变量、static方法、static语句块)。
static 关键字
static关键字的主要意义是在于创建独立于具体对象的域变量或者方法,以致于即使没有创建对象也没使用属性和调用方法。
static关键字还有一个比较关键的作用就是用来形成静态代码块以优化程序性能。static代码块可以置于类中的任何地方,类中可以有多个 static 块。在类初次被加载的时候,会按照 static 代码块的顺序来执行每个 static 块,并且只会执行一次。
为什么static代码块可以用来优化程序性能,是因为:它只会在类被加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在 static 代码块中被执行。
static 关键字的特点:
- 被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量或者方法不属于任何一个实例化对象,而是被该类的实例化对象所共享。
- 在该类第一次被加载的时候,就会去加载被 static 修饰的部分,而且只在类第一次使用时加载并进行初始化。
- static 变量在类加载的时候分配空间,以后创类对象的时候不会重新分配。
- 被 static 修饰的变量或者方法是优先于对象存在的,即就是当一个类被加载完毕后,即使没有创建对象,也可以访问。
static 应用场景:
- 修饰成员变量;
- 修饰成员方法
- 静态代码块
- 修饰类【只能修饰内部类,也就是静态内部类】
- 静态导包
注意事项:
- 静态只能访问静态;
- 非静态可以访问非静态,也可以访问静态。
三. 面向对象
1. 面向对象概述
面向对象和面向过程的区别
面向过程:
面向过程使具体化的,流程化的,解决一个问题,就需要一步一步的分析,一步一步的实现。
优点:性能比面向对象高,因为面向对象调用时需要实例化,开销较大,比较消耗资源。比如:单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象:
面向对象使模型化的,只需抽象出一个类,需要什么功能,不必一步一步去实现,只需要实例化一个实例,然后去调用相应的方法,不必去考虑这个功能使如何实现的。
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:系统相比于面向过程较低。
2. 面向对象的三大特性
三大特性基本描述
封装:
对类成员访问权限的设置,将类的属性和方法封装起来。隐藏对象的属性和实现细节,仅对外提供公共访问方式,便于使用,提高复用性和安全性。
继承:
对某一类的类的继承(子类继承父类)
- 子类拥有父类的非private的属性和方法;
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;
- 子类可以用自己的方式实现父类的方法——重写
- 方法名必须相同
- 子类访问权限不得低于父类
- 子类抛出的异常小于父类抛出的异常
- 参数列表相同
- Java是单继承
- 初始化顺序:父类对象——父类对象属性初始化——父类对象执行构造方法——子类对象——子类对象属性初始化——子类对象构造方法执行
多态:
同一行为具有不同的表现形式。多态就是同一个接口,使用不同的实例而执行不同的操作。
多态存在的三个条件:
- 有继承关系
- 子类重写父类的方法
- 父类引用指向子类对象(动态绑定)
比如:
Parent p = new Child();
当使用多态调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。(若没有,可以通过向下强制转型,将p的类型强转为子类类型)
什么是多态机制?
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法到底调用哪个类中的方法,必须在程序运行期间才能确定。
因为在程序运行时才确定具体的类,这样不用修改源程序代码就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态就是多态性。
3. 类与接口
抽象类与接口的对比
抽象类是用来捕捉子类的通用特性;接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种设计模板;接口是行为的抽象,是一种行为的规范。
相同点:
- 接口和抽象类都不能实例化
- 都位于继承的顶端,用于被其他类实现或继承
- 都包含抽象方法,其子类都必须覆写这些抽象方法。
不同点:
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract class关键字声明 | 接口使用interface声明 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,需要实现抽象类中所有声明的抽象方法 | 子类使用implements关键字来实现接口。子类如果不是抽象类的话需要实现接口中所有的抽象方法 |
构造器 | 抽象类可以有构造器 | 接口没有构造器 |
访问修饰符 | 任意访问修饰符 | 接口方法默认修饰符是public,并且不允许定义为其他的修饰符 |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 任意的 | 接口的字段默认都是static 和 final 的 |
普通方法 | 抽象类可以有普通方法 | 接口没有普通方法 |
在接口和抽象类的选择上,必须遵守的原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常都是优先选用接口,尽量少用抽象类。
- 选择抽象类的时候的场景通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
抽象类能使用final修饰吗?
不能。定义抽象类就是为了让其他类继承的,而一旦为 final 修饰的类就不能被继承,这样就会彼此矛盾,所以 final 不能修饰抽象类。
实例对象与对象引用有何不同?
new 创建好一个对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在占内存中)。一个对象引用可以指向 0 个 或者 1 个对象(一个绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 个绳子系住一个气球)
4. 变量和方法
成员变量和局部变量
在Java中定义一个没有参数且没有任何语句的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法,则会在编译时因为在父类中找不到不含参数的构造方法而发生错误。解决办法就是在父类中增加一个不含参数的构造方法。
类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能否正确执行?
构造方法的作用:完成该对象的初始化工作。
可以正常执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。
构造方法的特性
- 方法名与类名相同
- 没有返回值,且无需用 void 声明;
- 生成类的对象时自动执行,无序调用。
什么是方法的返回值?返回值的作用是什么?
方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果。
作用:接收输出结果,使得它可以继续用于其他操作。
5. 内部类
什么是内部类?
在Java中,可以将一个类的定义放在另一个类的定义内部,这个就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。
内部类的分类
内部类可以分为四种:成员内部类、局部内部类、匿名内部类、静态内部类。
静态内部类:
定义在类内部的静态类,就是静态内部类。
public class Outer{
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable: " + radius);
}
}
}
静态内部类可以访问外部类所有的静态变量,不可访问外部类的非静态变量。
静态内部类的创建方式,new 外部类.静态内部类(),如下:
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
成员内部类
定义在类内部,成员位置上的非静态类,就是成员内部类,
class Outer{
private static int radius = 1;
private int count = 2;
class Inner {
public void visit() {
System.out.println("visit outer static variable: " + radius);
System.out.println("visit outervariable: " + count);
}
}
}
成员内部类可以访问外部类所有的变量和方法,包括静态和非静态、私有和共有。
成员内部类的创建方式外部实例.new 内部类(),如下
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
局部内部类
定义在方法中的内部类,就是局部内部类。
匿名内部类:
匿名内部类就是没有名字的内部类,日常开发中使用比较多。
publilc class Outer {
private void test(final int i) {
new Service() {
public void method() {
for(int j = 0; j < i; j++){
System.out.println("匿名内部类");
}
}
}.method();
}
}
interface Service{
void method();
}
注意:匿名内部类的特点:
- 没有名字
- 匿名内部类必须继承一个抽象类或实现一个接口
- 匿名内部类不能定义任何静态成员和静态方法
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final
为什么要加final?
因为生命周期不一致,局部变量直接存储在栈中,当方法执行结束后,非 final 的局部变量就会被销毁。而局部变量内部类/匿名类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外部的局部变量区分开,解决这一问题。
内部类的优点
- 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据
- 内部类不为同一包的其他类所见,具有很好的封装性
- 内部类有效实现了“多重继承”,优化Java单继承的缺陷
- 匿名内部类可以很方便你的定义回调。
内部类有哪些应用场景
- 一些多算法场合
- 解决一些非面向对象的语句块
- 适当使用内部类,可以使得代码更加灵活,富有扩展性
- 当某个类除了它的外部类,其他类不需要使用时。
重写与重载
重载(Overload)与重写(Override)的区别。重载的方法能否根据返回值类型进行区分?
方法的重载和方法的重写都是实现多态的方法,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一类中;方法名相同,参数列表不同(参数类型不同、参数个数不同、参数顺序不同)被认为是重载;与方法返回值和访问修饰符无关;可以抛出不同的异常。
重写:发生在父子类中;方法名、参数列表、返回值类型都相同;如果父类方法访问修饰符为private,则子类不能进行方法的重写;构造方法、声明为final的方法、声明为static的方法都不可被重写;访问权限不能比父类中被重写的方法的访问权限更低;重写的方法不能抛出新的强制性异常。
为什么函数不能根据返回值类型来区分重载?
因为调用的时候不能制定类型信息,编译器并不知道你要调用的是哪个函数。
函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者之间进行通讯的关键。但并不能作为某个方法的“标识”。
对象相等判断
== 和 equals 的区别是什么?
首先,两者最大的区别,==是运算符,而equals() 是方法
==:它的作用是判断两个对象的地址是否相等。即,判断两个引用所指的对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型比较的是内存地址)
equals():判断两个对象是否相等。但它一般有两种使用情况。
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”来比较这两个对象。
情况2:类覆盖了 equals() 方法。判断 equals() 比较的两个对象的内容是否相等。
hashCode 与 equals
为什么重写 equals 方法时,,必须重写 hashCode方法?
hashCode() 介绍
hashCode() 的作用是获取散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出相应的“值”。这其中就利用到了散列码。
为何要有hashCode()?
我们以“HashSet如何检查重复”为例来说明为什么要有hashCode
当你把对象加入 HashSet时,HashSet会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashCode 值作对比,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值得对象,这时就会调用 equals() 方法来检查 hashCode 值相等的两个对象是否真的相等。如果二者相等,就会加入操作失败。如果不相等,就会重新散列到其他位置。这样就会大大减少 equals 的次数,从而大大提高了执行速度。
因此 equals 方法被覆写时,hashCode 方法也必须被覆盖。hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该类的两个镀锡无论如何都不会相等。
说明:
- 如果两个对象相等,则 hashCode 一定也是相等的;
- 如果对象相等,两个对象分别调用 equals 方法都返回 true;
- 两个对象有相同的 hashCode 值,他们不一定是相等的。
对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等比的是内存中存放的内容是否相等,引用相等比较的是他们指向的内存是否是同一块。
值传递
Java的传递?
当一个对象被当作参数传递给到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果 —— Java的传递是值传递。
Java语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象的改变是不会影响到调用者的。
Java为什么只有值传递?
Java 程序设计语言总是采用按值调用。也就是说方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。
example1:
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1); // num1 = 10;
System.out.println("num2 = " + num2); // num2 = 20;
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a); // a = 20
System.out.println("b = " + b); // b = 10
}
交换的是num1 和 num2 两个拷贝的值,所以num1 和 num2 的本身并不会改变。
example2:
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]); // 1
change(arr);
System.out.println(arr[0]); // 0
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的是同一个数组对象。因此,外部对引用对象的改变会反应到所对应的对象上。方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
example3:
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName()); // s1:小张
System.out.println("s2:" + s2.getName()); // s2:小李
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName()); // x:小李
System.out.println("y:" + y.getName()); //y:小张
}
}
IO 流
1. Java中 IO 流的分类
- 按照流的流向分为:输入流和输出流
- 按照操作单元分为:字节流和字符流
- 按照流的角色分为:节点流和处理流
Java IO 流都是从以下4个抽象基类派生出来的。
- InputStream / Reader :所有输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream / Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。
按照操作方式可划分为以下结构图。
2. Files 的常用方法
方法名 | 用途 |
---|---|
Files.exits() | 检查文件路径是否存在 |
Files.createFile() | 创建文件 |
Files.delete() | 删除一个文件后者目录 |
Files.copy() | 复制文件 |
Files.move() | 移动文件 |
Files.size() | 查看文件个数 |
Files.isDirectory() | 判定一个文件是目录 |
Files.isFiles() | 判定是否是文件 |
Files.mkdir() | 创建一个空目录 |
Files.mkdirs() | 创建目录(无论有多少级父目录,都会创建) |
Files.getParent() | 取得父路径 |
Files.getParentFile() | 取得 File 对象 |
Files.listFiles() | 列出一个目录指定的全部组成 |
Files.length() | 取得文件字节大小 |
字节流与字符流相互转换
序列化
反射
什么是反射?
Java反射机制是在运行状态下,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
静态编译和动态编译
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:在运行时确定类型,绑定对象
反射的过程
Java中的反射首先是能够获取到要反射类的字节码,获取字节码的方式有三种(1. Class.forName(className);2. 类名.class;3. this.getClass())。然后将字节码中的方法、变量、构造方法映射成相应的 Method、Field、Constructor 等类。
步骤:
- 写Java代码首先保存成 .java 文件
- 把 .java 文件编译成 .class 文件
- 使用类加载器把 .class 文件加载到内存中,然后使用 jvm 运行
- 在内存中的 .class 文件,使用 Class 类表示。而反射就是要得到类中的所有内容,所以要想使用反射,首先要得到类的 Class
反射的相关方法
方法 | 说明 |
---|---|
Method[] getMethods() | 得到所有的普通方法,返回方法的数组 |
Method getMethod(String name, Class parameterType) | 得到某一个指定的方法,两个参数:第一个 > 指定的方法名称,第二个 > 参数的Class类型。例如:getMethod(add,String.Class) |
String getName() | 得到方法的名称 |
Object invoke(Object obj, Object…args) | 运行某个方法,第一个参数 > 实例,第二个参数 > 参数。例如:invoke(对象, “zhangsan”) |
反射的优缺点
- 优点:运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点:性能瓶颈,反射相当于一系列结束操作,通知 JVM 要做的事,性能比直接的 Java 代码要慢很多
反射机制的应用场景
反射式框架设计的灵魂
模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制;还有日常使用的Spring / Hibernate 等框架也使用到了反射机制。
(1)我们在使用 JDBC 连接数据库的使用,使用 Class.forName() 通过反射加载数据库驱动程序。
(2)Spring 框架也用到很多的反射机制,最经典的就是 xml 的配置模式。
Java获取反射的三种方法
- 通过 new 对象实现反射机制 —— this.getClass()
- 通过路径实现反射机制 —— Class.forName(“className”)
- 通过类名实现反射机制 —— 类名.class
常用 API
1. String 相关
什么是字符串常量池
字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块内存存储相同的字符串。
在创建字符串时,JVM 会首先检查字符串常量池,如果该字符串已经存在池中,就返回他的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
String 的特性
- String 不是基本数据类型
- 不变性:String 是只读字符串,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要功能作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
- 常量池优化行:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
- 被 final 修饰:使用 final 关键字来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
String 类的源码如下,String 就是利用了 final 修饰的char 类型数组存储字符。
private final char value[];
String 真的是不可变的吗?
一般来说,String 类型的对象就是不可变的,如果对字符串进行了 “ + 某段字符串”,字符串对象原本在常量池中的字符串并没有变长,只是 str 重新指向了常量池中的变长后的对象。
通过反射是可以修改所谓的“不可变”对象的,但是我们不一般不会去这么做
String s = "Hello World";
System.out.prtinln("s = " + s); // Hello World
// 获取 String 类中的 value 字段 —— 反射中的属性
Field valueFieldOfString = String.class.getDeclareddField("value");
// 改变 value 属性的访问权限
valueFieldOfString.setAccessible(true);
// 获取 s 对象上的 value 属性的值
char[] value = (char[])valueFieldOfString.get(s);
// 改变 value 所引用的数组的第 5 个字符
value[5] = '_';
System.out.prtinln("s = " + s); // Hello_World
解释:
用反射可以访问私有成员,然后反射出 String 对象中的value 属性,进而通过改变 value 属性,从而改变 String。
String str = “i” 与 String str = new String(“i”)一样吗?
两者并不一样,因为内存的分配方式不同。
前者创建生成的 str 指向静态区(字符常量区)。
后者创建生成的 str 指向堆内存;且后者在创建过程中生成了两个字符串对象,一个是字符常量区的"i",一个是用 new 创建在堆上的对象
String str1 = "hello"; //str1指向静态区
String str2 = new String("hello"); //str2指向堆上的对象
String str3 = "hello";
String str4 = new String("hello");
System.out.println(str1.equals(str2)); //true
System.out.println(str2.equals(str4)); //true
System.out.println(str1 == str3); //true
System.out.println(str1 == str2); //false
System.out.println(str2 == str4); //false
System.out.println(str2 == "hello"); //false
str2 = str1;
System.out.println(str2 == "hello"); //true
如何将字符串反转?
StringBuilder 或者 StringBuffer 中都有 reverse() 方法,可以直接实现字符串反转
String 类常用的方法
方法 | 说明 |
---|---|
int indexOf(char / s) | 返回指定字符 / 字符串的索引,还可以指定开始的位置:第二个参数为开始的索引值,查不到返回-1 |
int lastIndexOf(char / s) | 从后向前查找字符 / 字符串位置,还可以指定开始的位置:第二个参数为开始的索引值 |
char charAt(index) | 返回指定索引处的字符 |
void replaceAll(regex, replacement) | 替换所有指定的内容 |
void replaceFirst(regex, replacement) | 替换首个内容 |
boolean equals(s) | 区分大小写比较两个字符串内容是否相等 |
int compareTo(s) | 比较两个字符串大小比较 |
boolean startsWith(s) | 判断是否是以指定字符串开头,还可以指定开始的位置:第二个参数为开始的索引值 |
boolean endsWith(s) | 判断是否是以指定字符串结尾 |
String[] split(s) | 将字符串拆分 |
String join() | 链接,静态方法,用类名调用 |
String substring(int begin, int enf) | 截取部分内容 【前闭后开】 |
String trim() | 去掉字符串中的左右空格,中间空格保留 |
String toUpperCase() | 字符串全部转成大写 |
String toLowerCase() | 字符串全部转成小写 |
byte[] getBytes() | 返回字符串的 byte 类型数组 |
与 String 相比,StringBuilder 和 StringBuffer增加了两个常用方法
方法 | 说明 |
---|---|
StringBuilder append(s) | 字符串拼接 |
String toString | 转换成 String |
在使用 HashMap 的时候,用String做key有什么好处?
HashMap 内部实现是通过 key 的hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
String 和 StringBuffer、StringBuilder 的区别是什么?
可变性
String 类中使用字符数组 char[] value 保存字符串,且被 private final 修饰,因此 String 对象是不可变的。
StringBuilder 和 StringBuffer 都继承自 AbstractStringBuilder 类,在AbstractStringBuilder 中也是使用字符串数组保存字符串,但他没有被 final 修饰,因此这两种对象都是可变的。
线程安全性
String中的对象是不可变的,也就可以理解为常量,因此它的线程就是安全的。而StringBuilder 和 StringBuffer 是可变的,在此基础上,StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以线程是安全的,但是 StringBuilder 并没有对方法加同步锁,所以线程并不安全
性能
对 String类型的对象进行改变的时候都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都是对 StringBuffer 对象本身进行操作,并不用生成新的对象。相同情况下,StringBuilder 比 StringBuffer 的性能更高些,但他的线程不是特别安全。
总结
少量数据 —— String
单线程操作大量数据 —— StringBuilder
多线程操作大量数据 —— StringBuffer
包装类相关
自动装箱 与 拆箱
装箱:将基本类型用他们对应的引用类型包装起来。
拆箱:将包装类型转化为基本数据类型。
int 和 Integer 有什么区别
Java是面向对象的编程语言,在平时的使用中为了方便引入了基本类型,但是为了能够将这些基本数据类型当成对象操作,Java为每个基本数据类型引入了包装类型。这里的 Integer 就是 int 的包装类,即,int 是基本数据类型,Integer 是引用数据类型。从Java 5开始引入了自动装箱和拆箱机制,使得二者可以相互转换。
Integer a = 127 与 Integer b = 127 相等吗?
如果整型字面量的值在 -128~127 之间,那么自动装箱时不会 new 新的 Integer 对象,而是直接引用常量池中的 Integer 对象,超过范围两者的结果就是 false
public static void main(String[] args) {
Integer a = new Integer(3);
// 将3自动装箱成Integer类型
// int 数据赋值给Integer,触发装箱,Integer.valueOf(b)
Integer b = 3;
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true
Integer a1 = 128;
Integer b1 = 128;
System.out.println(a1 == b1); // false
Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}
==比较
对于引用类型:比较的是对象的内存地址
对于数据类型:比较的是值的大小
equals() 比较
引用类型:1)先比较二者的引用类型是否相同;2)再比较二者的内容是否相同。
未完待续