什么是面向对象?
面向对象是一种基于面向过程的编程思想,是向现实世界模型的自然延伸,这是一种“万物皆对象”
的编程思想。由执行者变为指挥者,在现实⽣生活中的任何物体都可以归一类事物,而一个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动。
什么是对象?
我个人的理解是一个实例的所有属性的综合
,比如说一个学生,当创建一个学生的对象,那就需要学生的就读学校,就读班级,定位学生的学号,学生的学习成绩,学生的授课老师,这些综合起来就可以描述一个学生的群体,反正总的一句话,万事万物皆对象
。
面向对象和面向过程的区别
编程思路不同:面向过程以实现功能的函数开发为主,而面向对象要首先抽象出类、属性及其⽅方法,然后通过实例化类、执行方法来完成功能。
封装性:都具有封装性,但是面向过程是封装的是功能,而面向对象封装的是数据和功能。
面向对象具有继承性和多态性,而面向过程没有继承性和多态性,所以面向对象优势很明显。
java语言的三大特性
1、封装
把对象的基本属性和基于属性的操作封装起来,使其构造出一个不可分割的独立整体,数据被保护在对象的内部,尽可能地隐藏内部的细节,使用者不需要知道里面的细节,这个时候对数据的访问或赋值等操作就只能通过已经定义的接口进行访问。
优点
减少耦合:可以独立开发,测试,优化,修改等 。 减轻维护的负担:更容易被理解,而且可以独立调试不影响其他的模块,提高了软件的可用性 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的
实例代码: 封装的Teacher类中包含有name和subject属性,外界只能通过已经封装好的get和set方法访问Teacher类中的属性。
public class Teacher {
private String name;
private String subject;
public Teacher() {
}
public Teacher(String name, String subject) {
this.name = name;
this.subject = subject;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void teach(){
System.out.println(name + "老师教导"+ subject);
}
private String teachNo(){
return "没有老师教学。";
}
public static void main(String[] args) {
// 创建对象
Teacher teacher = new Teacher("小红","数学");
// 通过已经定义的接口进行修改属性值
teacher.setName("小明");
// 打印验证
teacher.teach();
}
}
结果:
2、继承
继承指的是从已有的类中得到继承信息并创建新类的过程,提供继承信息的类被称作父类(超类/基类),得到继承信息的被称为子类(派生类)。继承实现了IS-A关系,继承应当遵循里氏替换原则,子类对象必须能够替换掉父类的所有对象。
实例代码: 创建父类Person对象
public class Person {
private int age;
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// public int getAge() {
// return age;
// }
private int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void speak(){
System.out.println("我的名字是" + name + "我今年" + age + "岁了");
}
}
创建子类Student对象
public class Student extends Person{
private String id;
private String subject;
public Student() {
}
public Student(String id, String subject) {
this.id = id;
this.subject = subject;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public static void main(String[] args) {
Student student = new Student();
// 子类可以通过封装的方法访问到父类和之类的所有get和set方法。
student.setId("123456789");
student.setName("小红");
student.setAge(18);
student.setSubject("语文");
System.out.println(student.id);
System.out.println(student.name);
System.out.println(student.getAge());
System.out.println(student.subject);
// 报错:子类无法获取到父类的私有属性和私有方法
// System.out.println(student.age);
// System.out.println(student.getAge());
}
}
结果:
代码分析: Student类继承Person类,Person是超类(基类),而Student类是子类(派生类),Student类继承自Person类,因此Student类对象 可以访问到超类的所有非private属性或者方法。
动态绑定和静态绑定
绑定:把一个方法与其所在类/对象关联起来叫做绑定,绑定分为前期绑定(静态绑定)和后期绑定(动态绑定)。
静态绑定
静态绑定(前期绑定)指的是程序在运行前就知道这个方法是属于哪个类,在编译时就可以直接连接到这个类中,直接定位到这个方法里面。在java中,比如说final,private,static等修饰的方法和构造函数都是静态绑定的,不需要程序运行,也可以不需要具体的实例对象就可以知道这个方法的具体内容。 实例代码:
public class Car {
// 静态绑定
private int price;
// 静态绑定
public int getPrice() {
return price;
}
// 静态绑定
public void setPrice(int price) {
this.price = price;
}
// 静态绑定
static {
System.out.println("我是静态代码块!");
}
public void move(){
System.out.println("Moving");
}
public static void main(String[] args) {
Car car = new Car();
car.setPrice(300);
System.out.println("这辆车的价格是:" + car.getPrice());
}
}
结果:
动态绑定
动态绑定指的是程序在运行过程中,需要具体的实例对象才能确定具体是哪一个方法,简单来说在编译时期不知道这个方法是属于哪个对象的,需要等对象创建出来,通过对象实例才能实现方法调用。
动态绑定的过程:
虚拟机提取对象的实际类型的方法表。 虚拟机搜索方法签名 。 调用方法
实例代码:
public class Head {
public void initialization(){
System.out.println("脑袋初始化完毕!已苏醒!");
}
}
public class Mouth extends Head{
public void initialization(){
System.out.println("嘴巴初始化完毕!");
}
public void eat(){
initialization();
System.out.println("嘴巴正在吃东西");
}
public static void main(String[] args) {
Mouth mouth = new Mouth();
mouth.eat();
}
}
结果:
向上转型
以父类对象当作引用对象同时创建子类对象,简单来说就是左边的引用参数是父类,右边创建对象的参数是子类,这就叫向上转型,同时因为引用对象是父类的对象,所以只能调用父类的方法,如果子类重写了父类的方法,那么程序在执行的过程中会优先寻找子类已经重写的方法,如果找不到,那就向上转型寻找父类的方法。同时父类可以是类或者是接口(接口只能定义常量和方法,不可以定义变量)
实例代码:
public class Person {
int age;
String name = " 父类属性 ";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
System.out.println("我是父类方法,修改age值:" + age);
this.age = age;
}
public void speak(){
System.out.println("我的名字是" + name + "我今年" + age + "岁了");
}
}
public class Student extends Person{
String name = "子类属性";
public String getName() {
System.out.println("我是子类方法");
return name;
}
public void setName(String name) {
System.out.println("我是子类方法,修改name值:" + name);
this.name = this.name + name;
System.out.println(this.name);
}
public static void main(String[] args) {
Person person = new Student();
person.setAge(32);
person.setName("小红");
person.speak();
}
}
结果:
代码分析: 代码中修改了父类的age和name属性值,子类重写了name属性的get和set方法,因此调用的是子类重写的方法,而修改age值调用的是父类的方法。另外属性值的使用,子类中也包含有name属性,但是在修改使用name属性的过程中,程序优先使用父类的属性。
向下转型
第一种情况: 父类的引用对象指向的是子类创建的对象,那么在向下转型的过程中是安全的,在编译和运行的过程中都不会发生异常。
实例代码: 父类为Animal类
public class Animal {
private String type;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
子类为Cat类
public class Cat extends Animal{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "我是一只名字叫" + name + "的猫" + "我可以在" + getType() + "运动";
}
}
测试调用类为Dynamic类
public class DynamicTest {
public static void main(String[] args) {
// 向上转型,创建的实例对象为子类的对象
Animal animal = new Cat();
// 向下转型,不会出现任何问题
Cat cat = (Cat) animal;
cat.setName("汤姆");
cat.setType("地上");
System.out.println( cat.toString());;
}
}
结果:
第二种情况: 父类的引用对象是父类本身,那么在向下转型的过程中是不安全的,程序在编译的时候不会出错,但是在运行的时候会发生
java.lang.ClassCastException
异常,这个时候就需要instanceof关键字
解决异常。instanceof关键字
通过返回一个布尔值来比较出这个对象是否是这个特定类或者是它的子类的一个实例。
实例代码:
public class Bird extends Animal{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "我是一只名字叫" + name + "的鸟" + "我可以在 " + getType() + "运动";
}
}
public class DynamicTest {
public static void main(String[] args) {
Animal animal = new Animal();
if(animal instanceof Cat){
Cat cat = (Cat) animal;
cat.setName("汤姆");
cat.setType("地上");
System.out.println(cat.toString());
}else if(animal instanceof Bird){
Bird bird = (Bird) animal;
bird.setName("咕咕");
bird.setType("天上");
System.out.println(bird.toString());
}
}
}
代码分析:
上面的代码不会有执行结果的,因为创建的对象实例是父类Animal类,它经过if else 语句的时候直接跳过运行,全部为false。如果说将第一行代码改为Animal animal = new Bird();
,结果为:
总结
父类的引用可以指向子类,但是子类的引用不可以指向父类,一旦这样操作,由于对象的继承关系,程序在编译的时候不会发生异常,但是在运行的过程中会
java.lang.ClassCastException
异常。
在向上转型的过程中,对象引用不可以调用子类的新生的方法,只能调用父类的实现方法,但是子类中如果重写有父类的方法,对象引用还是可以调用重写后的方法的。
在向下转型的过程中,如果父类引用指向的是子类的对象创建,那么程序在编译和运行阶段都可以正常运行,如果父类引用指向的是父类的对象创建,将对象引用指向子类,编译的时候不会会发生异常,但是在运行阶段会发生
java.lang.ClassCastException
异常。
程序可以先向上转型,然后再向下转型,比如上文中向下转型的第一种情况。但是不可以一开始就向下转型。
多态
多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性
和运行时的多态性
。
运行时多态
方法重写( override )实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现多态需要三个条件∶
1.方法重写
:子类继承父类并重写父类中已有的或抽象的方法;
2.对象造型
:用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。也就是向上转型。
3继承
:继承就是重已有的类中得到信息并创建新类的过程
编译时多态
方法重载( overload )实现的是编译时的多态性(也称为前绑定),在java程序中,使用final,static,private修饰的方法,属性,类(static可以修饰内部类)在编译的时候就确定了是哪个对象里面的内容。
实例代码:
public class University {
private String address;
private String name;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "大学的地址是" + address + ", 名字叫作:" + name;
}
}
public class Teacher extends University{
private String name;
private String subject;
public Teacher() {
}
public Teacher(String name, String subject) {
this.name = name;
this.subject = subject;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public void teach(){
System.out.println(name + "老师教导"+ subject);
}
public static void main(String[] args) {
// 创建对象
Teacher teacher = new Teacher("小红","数学");
teacher.setAddress("中国湖南");
teacher.setName("超级大学");
System.out.println(teacher.toString());
teacher.teach();
}
}
结果:
java的基本数据类型
基本类型 | 位数 | 字节 | 默认值 | 取值范围 |
---|---|---|---|---|
byte | 8 | 1 | 0 | -128 ~ 127 |
short | 16 | 2 | 0 | -32768 ~ 32767 |
int | 32 | 4 | 0 | -2147483648 ~ 2147483647 |
long | 64 | 8 | 0L | -9223372036854775808 ~ 9223372036854775807 |
char | 16 | 2 | 'u0000' | 0 ~ 65535 |
boolean | 1 | false | false、true | |
float | 32 | 4 | 0f | 1.4E-45 ~ 3.4028235E38 |
double | 64 | 8 | 0d | 4.9E-324 ~ 1.7976931348623157E308 |
byte数据类型
byte数据类型是8位的,有符号,以二进制补码表示的整数。 最小值:-128(-2^7) 最大值:127(2^7-1) 默认值: 0 byte类型的应用在大型数组中节约空间,主要替代整数,因为byte变量占用的空间只有int类型的四分之一。
byte a = 100; byte b = -50;
short数据类型
short数据类型是16位,有符号的以二进制补码表示整数。 最小值: -32768(-2^15) 最大值: 32767(-2^15-1) short可以像byte那样节约空间,一个short类型的数据占的内存空间是int类型的二分之一。 默认值: 0
short a = 1000; short r = -200000;
int数据类型
int 数据类型是32位、有符号的以二进制补码表示的整数; 最小值: -2,147,483,648(-2^31); 最大值: 2,147,483,647(2^31 - 1); 一般地整型变量默认为 int 类型; 默认值是 0 ;
int a = 1; int b = -100;
long 数据类型
long 数据类型是 64 位、有符号的以二进制补码表示的整数; 最小值: -9,223,372,036,854,775,808(-2^63); 最大值: 9,223,372,036,854,775,807(2^63 -1); 这种类型主要使用在需要比较大整数的系统上; 默认值是 0L;
long a = 100000L,Long b = -200000L。
float 数据类型
float 数据类型是单精度、32位、符合IEEE 754标准的浮点数; float 在储存大型浮点数组的时候可节省内存空间; 默认值: 0.0f; 浮点数不能用来表示精确的值,如货币;
float f1 = 234.5f
double数据类型
double 数据类型是双精度、64 位、符合IEEE 754标准的浮点数; 浮点数的默认类型为double类型; double类型同样不能表示精确的值,如货币; 默认值是 0.0d;
double d1 = 123.4。
char 数据类型
char类型是一个单一的 16 位 Unicode 字符; 最小值: \u0000(即为 0); 最大值: \uffff(即为 65535); char 数据类型可以储存任何字符;
char letter = 'A';(单引号)
boolean 数据类型
boolean数据类型表示一位的信息; 只有两个取值:true 和 false; 这种类型只作为一种标志来记录 true/false 情况; 默认值是 false;
boolean one = true。
注意: 在java中使用long数据类型的一定要在数值的后面添加上L,否则程序会将该变量当作整形变量解析。"L"理论上不分大小写,但是若写成"l"容易与数字"1"混淆,不容易分辩,所以最好大写。这八种基本类型都有对应的包装类分别为:Byte、Short、Integer、Long、Float、Double、Character、Boolean。
自动装箱和拆箱
自动装箱与拆箱: Java 编译器在基本数据类型和对应的包装类之间做的一个转化。例如如:把 int 转化成 Integer, double转化成 Double 等等。反之就是自动拆箱。 原始类型: boolean、 char、 byte、 short、 int、 long、 float、 double 封装类型: Boolean、 Character、 Byte、 Short、 Integer、 Long、 Float、 Double 实例代码:
public class Demo {
public static void main(String[] args){
Integer a = 100;
int b = a;
}
}
字节码文件:
0 bipush 100
2 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
5 astore_1
6 aload_1
7 invokevirtual #3 <java/lang/Integer.intValue : ()I>
10 istore_2
11 return
代码分析: 从字节码文件中可以得出当程序需要装箱时,就会调用对应的包装类的valueof 方法,而拆箱就是调用对应的xxxValue方法。由此可以得出上面的两行代码可以等价于下面的代码。
Integer a = 100 等价于:Integer a = Integer.valueOf(100);
int b = a 等价于: int b = a.intValue();
java的标识符和关键字
在程序中,我们需要不断地创建名字用以命名类和变量等,这些就叫标识符,但是在程序中有一些标识符已经被内部赋予了特殊的含义了,这些包含有特殊的含义的标识符就是关键字。比如生活中人们开一家店,可以命名为杂货店或是其他名字,但是就是不可以命名为警察局或者是派出所等等。
java的关键字
访问控制关键字
private关键字: private修饰的方法或者是属性只允许当前类访问,不允许其他任何的访问途径,子类是可以继承并访问父类的所有非private的方法和属性。
public关键字: public关键字可以修饰类,一旦使用public修饰类,则该类的源文件要与类名相同,否则编译不通过。一个类是作为外部类的时候,只能被public修饰或者不写修饰符。如果类作为内部类,那么可以使用四种修饰符修饰,因为此时的类是作为外部类的属性而存在的。public修饰的属性和方法可以被任何类访问,通过创建该类的对象可以对public修饰的方法和属性进行操作。另外,使用public修饰的属性和方法也可以通过子类继承下来。对方法而言,如果子类重写了父类的方法,子类对象访问该方法的时候优先访问子类的重写的方法,但是属性相反,子类对象优先访问父类的属性。
protected关键字: protected定义的是受保护的,受到该类所在的包的保护。被protected修饰的属性和方法可以被该包下的所有类访问到,同时也可以被该类的所有子类继承下来,也就是说子类的实例可以访问父类的所有使用protected修饰的属性和方法。(子类的范围指的是同一个包下的所有的子类或者是不同包下的所有子类)
“friendly”: 定义为友好的,也就是不写任何的修饰符。对于该属性或方法,在同一个包中的所有类都可以访问,同时默认的权限修饰的属性和方法也可以被同一个包下的子类继承,也就是说只有同一个包下的子类才可以访问到父类的默认属性和方法。
其他关键字
java的运算符
算术运算符
Java 支持所有的基本算术运算符 ,这些算术运算符用于执行基本的数学运算 ,常见的运算符有:加、减、乘、 除和求余等。
加法:
double a = 5 . 2 ;
double b = 3 . 1;
double sum = a + b ;
// sum 的值为 8 . 3
System.out.println(sum);
// + 还可以作为字符串的连接运算符 。
减法:
double a = 5 . 2 ;
double b = 3 .1;
doub1e sub = a - b ;
// sub 的值为 2 . 1
System.out.println(sub);
乘法:
double a = 5 . 2 ;
double b = 3 . 1 ;
double multiply = a * b ;
// multiply 的值为 16.12
System.out.println(mu1tiply);
除法: 对于除法,首先除数不可以是0,否则编译运行会出现问题,其次,当两个操作数都是整数类型的时候,得到的商也是整数,不满1的自动去除。比如说19/4得到的是4而不是5。如果两个操作数或者一个操作数是浮点数,那得到的结果也是浮点数。
public class DivTest{
public static void main(String[] args){
double a = 5.2;
double b = 3.1;
double div = a / b;
// div的值将是1.6774193548387097
System.out.println(div);
// 输出正无穷大:Infinity
System.out.println("5除以0.0的结果是:" + 5 / 0.0);
// 输出负无穷大:-Infinity
System.out.println("-5除以0.0的结果是:" + - 5 / 0.0);
// 下面代码将出现异常
// java.lang.ArithmeticException: / by zero
System.out.println("-5除以0的结果是::" + -5 / 0);
}
}
求余: 求余运算符的用法跟除法的类似,它的计算结果是使用第一个操作数除以第二个操作数 , 得到一个整除的结果后剩下的值就是余数。
public class ModTest{
public static void main(String[] args){
double a = 5.2;
double b = 3.1;
double mod = a % b;
System.out.println(mod); // mod的值为2.1
System.out.println("5对0.0求余的结果是:" + 5 % 0.0); // 输出非数:NaN
System.out.println("-5.0对0求余的结果是:" + -5.0 % 0); // 输出非数:NaN
System.out.println("0对5.0求余的结果是:" + 0 % 5.0); // 输出0.0
System.out.println("0对0.0求余的结果是:" + 0 % 0.0); // 输出非数:NaN
// 下面代码将出现异常:java.lang.ArithmeticException: / by zero
System.out.println("-5对0求余的结果是:" + -5 % 0);
}
}
自增和自减运算符: 自增(++)和自减(--)运算符有两个特点,其一,这运算符是单目运算符,只能操作一个操作数,其二,运算符只能操作单个数值类型(整形、浮点型)的变量,不可以操作常量或者是表达式。同时两种运算符可以出现在变量的左边或者是右边。如果运算符在变量的左边,那左边先执行操作(自加或者自减),然后才把变量放入到表达式中运算,如果运算符在变量的右边,先把变量放入到表达式中运算,然后再执行操作(自加或者自减)。例如下面的代码:
int a = 5 ;
//让 a 先执行算术运算,然后自加
int b = a++ + 6;
//输出 a 的值为 6 , b 的值为 11
System . out.print1n(a + " \ n " + b);
int a = 5 ;
//让 a 先自加,然后执行算术运算
int b = ++a + 6;
//输出 a 的值为 6 , b 的值为 12
System.out .print1n(a + " \ n " + b);
比较运算符
1、>:大于,只支持左右两边操作数是数值类型。如果前面变量的值大于后面变量 的值,则返回 true 。
2、>= :大于等于 , 只支持左右两边操作数是数值类型。如果前面变量的值大于等于后面变量的值,则返回 true。
3、<: 小 于,只支持左右两边操作数是数值类型。如果前面变量的值小于后面变量的值, 则返回 true 。
4、<=:小于等于,只支持左右两边操作数是数值类型。如果前面变量的值小于等于后面变量 的值,则返回 true 。
5、 ==: 等,如果进行比较的两个操作数都是数值类型,即使它们的数据类型不相同 ,只要它们的值相等,也都将返回 true 。 例如 97 == 'a'返回true。 5.0 == 5 也返回 true。 如果两个操作数都是引用类型,那么只有当两个引用变量的类型具有父子关系时才可以比较,而且这两个引用必须指向同一个对象才会返回true。Java 也支持两个 boolean 类型的值进 比较,例如. true = false将返回 false。
6、!=:不等于,如果进行 比较的两个操作数都是数值类型,无论它们的数据类型是否相同 , 只要它们的值不相等,也都将返回 true 。 如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同 一个对象就会返回 true 。
逻辑运算符
逻辑运算符用于操作两个boolean类型的变量或者是常量,常见的逻辑运算符有:
&&: 与,前后两个操作数必须都是 true 才返回 true , 否则返回 false 。 &:不短路与,作用与 &&相同,但不会短路 。 || : 或,只要两个操作数中有一个是 true,就可以返回 true,否则返回 false |: 不短路或,作用与 || 相同,但不会短路 。 !: 非,只需要 一个操作数,如果操作数为 true ,则返回 false ; 如果操作数为 false ,则返回 true ^ : 异或,当两个操作数不同时才返回 true ,如果两个操作数相同则返回 false 。
实例代码:
public class LogicOperatorTest{
public static void main(String[] args){
// 直接对false求非运算,将返回true
System.out.println(!false);
// 5>3返回true,'6'转换为整数54,'6'>10返回true,求与后返回true
System.out.println(5 > 3 && '6' > 10);
// 4>=5返回false,'c'>'a'返回true。求或后返回true
System.out.println(4 >= 5 || 'c' > 'a');
// 4>=5返回false,'c'>'a'返回true。两个不同的操作数求异或返回true
System.out.println(4 >= 5 ^ 'c' > 'a');
// 定义变量a,b,并为两个变量赋值
int a = 5;
int b = 10;
// 对a > 4和b++ > 10求或运算
if (a > 4 | b++ > 10){
// 输出a的值是5,b的值是11。
System.out.println("a的值是:" + a + ",b的值是:" + b);
}
// 定义变量c,d,并为两个变量赋值
int c = 5;
int d = 10;
// c > 4 || d++ > 10求或运算
if (c > 4 || d++ > 10){
// 输出c的值是5,d的值是10。
System.out.println("c的值是:" + c + ",d的值是:" + d);
}
}
位运算符
位运算的种类:
& :按位与 。当 两位同时为 1 时才返回 1 。 | : 按位或 。只要有一位为 1 即可返回 1 。 ~ :按位非。单目运算符,将操作数的每个位(包括符号位 )全部取反 。 ^ :按位异或 。 当两位相同时返回 0 , 不同时返回 1 。 <<:左移运算符 ,>> :右移运算符 ,>>> : 无符号右移运算符 。
一般的位运算法则:
基本数据类型的位运算法则:
1、对于低于int类型的(比如说byte类型、short类型、和char类型)操作数,运算是总是先转换为int类型后再移位。
2、对于int类型的整数移位,a>>b,当b>32时,系统首先使用b对32求余,(因为int类型的最大值只有32位)得到的结果才是真正的移位的位数。例如, a>>33 和a>>1 的结果完全一样,而 a>>32 的结果和 a 相同 。
3、对于 long 类型的整数移位 a>>b ,当 b>64 时,总是先用 b 对 64 求余(因为 long 类型是 64 位) ,得到的结果才是真正移位的位数 。
运算符的优先级
java语言的方法
方法的参数按值传递: 在方法中参数变量的使用方法和局部变量相同,唯一不同的是参数变量的初始值是由调用方提供的。方法处理的是参数的值,而非参数本身。 方法名可以被重载: Java的Math包使用这种方法为所有的原始数值类型实现了Math.abs()、Math.min()和 Math.max()函数。 方法只能返回一个值,但可以包含多个返回语句: 一个Java方法只能返回一个值,它的类型是方法签名中声明的类型。静态方法第一次执行到一条返回语句时控制权将会回到调用代码中。尽管可能存在多条返回语句,任何静态方法每次都只会返回一个值,即被执行的第一条返回语句的参数。 方法可以产生副作用: 方法的返回值可以是void,这表示该方法没有返值。
静态方法和非静态方法
首先静态方法不可以调用非静态成员,静态方法是属于一个类的内容,在类加载的时候就会被分配内存,而非静态成员属于实例对象,只有在类实例化之后才会存在,然后通过实例对象调用。由此就可以看出静态成员和非静态成员的区别,在类没有实例化的时候,静态方法就已经存在了,这个时候可以通过类名加方法名调用,但是此时内存中还不存在非静态成员,这个时候调用非静态成员属于非法操作。
两者的区别
1、调用方式不同
调用静态方法可以不需要等到类的实例化,可以直接通过类名.方法名
这种方法调用,但是调用非静态方法必须要等到类的实例化之后,通过对象.方法名
这种方法调用。但是不推荐使用对象.静态方法
这种方法调用静态方法,这样子容易造成混扰,因为静态方法不属于这个类的对象,而是属于这个类的。
2、访问权限不同
静态方法在访问这个类的成员的时候只能访问这个类的静态成员(静态方法或者静态变量
)而不能访问其他非静态成员(实例成员变量或者成员方法
),但是实例方法不存在这个限制。
方法重载和方法重写
重载: 编译时多态
、同一个类中同名的方法具有不同的参数列表、不能根据返回类型进行区分(函数调用时不能指定类型信息,编译器不知道要调用的是哪个函数);
重写(覆盖): 运行时多态
,子类与父类之间、子类重写父类的方法具有相同的返回类型、更好的访问权限。
JDK和JRE和JVM的区别
JDK
JDK(Java SE Development Kit),java标准的开发包,提供了编译,运行java程序所需要的各种工具和资源,包括了java编译器,java的运行环境,以及常用的java的类库
JRE
JRE(Java Runntime Environment),java运行时环境,用于解析执行java的字节码文件,普通的用户只需要安装JRE来运行java程序即可,但是作为一名java的开发者,必须要安装JDK来编译和调试程序。
JVM
JVM(Java Virtual Menchinal),JVM是运行java字节码文件的虚拟机,同时也是JRE的一部分,它是整个java实现跨平台开发的关键,负责解析执行字节码文件。所有平台上的java虚拟机都向编译器提供相同的接口,因此编译器只需要面向虚拟机,生成虚拟机可以字节码代码文件,由虚拟机执行即可。
什么是跨平台性?
跨平台性: 使用java语言编写的程序,可以实现一次编译在多个平台系统上运行。 实现原理: Java程序是通过java虚拟机在平台系统上运行的,只要该系统可以安装相对应的java虚拟机,那么这个系统就可以运行java程序。
为什么要采用字节码?
在java的运行过程中,JVM 可以理解的代码文件为字节码(即java程序经过java虚拟机编译器编译后扩展名为.class的文件),它不面向任何的特定的处理器,java通过字节码的方式在一定程序上解决了运行效率低下的问题,同时又保留了解析型语言可移植性的特点。因此java程序运行比较高效而且可以在不同的平台上运行。
Object类的公有方法解析
Object类的常见API方法详解:
protected Object clone():创建并返回此对象的副本。
boolean equals(Object obj):指示某个其他对象是否“等于”此对象。
protected void finalize():已过时(Java11官方API文档)。
类<?> getClass():返回此 Object的运行时类。
int hashCode():返回对象的哈希码值。
void notify():唤醒正在此对象监视器上等待的单个线程。
void notifyAll():唤醒等待此对象监视器的所有线程。
String toString():返回对象的字符串表示形式。
void wait():导致当前线程等待它被唤醒,通常是 通知或中断 。
void wait(long timeoutMillis):导致当前线程等待它被唤醒,通常是通知或中断,或者直到经过一定量的实时。
void wait(long timeoutMillis, int nanos):导致当前线程等待它被唤醒,通常是通知或中断 ,或者直到经过一定量的实时。
java创建对象的几种方式
java的对象创建方式有四种,如图所示:
使用new关键字
java创建对象最常用的方法是使用new关键字,使用new关键字可以创建一个对象实例,在创建的过程中可以调用无参构造方法或者是有参构造方法。(如果类中实现了有参构造方法,那么建议重写无参构造方法)
实例代码:
public class Demo {
private int a;
public Demo(int a){
this.a = a;
}
public static void main(String[] args){
Demo demo = new Demo(123);
System.out.println(demo.a);
System.out.println(demo.toString());
}
}
结果:
通过反射机制
使用Class类的newInstance方法
使用Class类的newInstance()
方法是使用反射创建对象最常用的方法之一,使用这个方法调用的是类的公有的无参数构造器
,即创建对象的这个类要包含有公有无参构造方法
,否则无法成功调用,运行报错。
实例代码1:
public class Demo {
public void test(){
int a = 100;
a ++;
System.out.println("a的值为: " + a);
}
public static void main(String[] args) throws Exception {
Demo demo = Demo.class.newInstance();
demo.test();
System.out.println(demo.toString());
}
}
结果:
实例代码2:
public class Demo {
public Demo(int a){
}
public void test(){
int a = 100;
a ++;
System.out.println("a的值为: " + a);
}
public static void main(String[] args) throws Exception {
Demo demo = Demo.class.newInstance();
demo.test();
System.out.println(demo.toString());
}
}
结果:
使用Construct类的newInstance方法
Construct类中包含有一个newInstance方法,这个方法与Class类的newInstance方法很相似,但是Construct类的newInstance方法可以调用类的有参构造方法(可以不是无参数的构造方法)或者是私有的构造方法。
实例代码:
import java.lang.reflect.Constructor;
class Test{
private String name;
private Test() {
System.out.println("无参数构造方法");
}
public Test(String name) {
System.out.println("有参数构造方法");
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
public class Demo {
public static void main(String[] args) throws Exception {
// 可以获取到public的或者是private的构造方法
Constructor<?>[] constructors = Test.class.getDeclaredConstructors();
// 只返回public的构造方法
Constructor<?>[] constructors1 = Test.class.getConstructors();
// 打印验证
for (Constructor temp: constructors) {
System.out.println(temp);
}
System.out.println();
for (Constructor temp1: constructors1) {
System.out.println(temp1);
}
System.out.println();
Constructor<?> noAccess = constructors[0];
Constructor<?> canAccess = constructors[1];
// 设置私有可以访问
noAccess.setAccessible(true);
// 私有无参构造
Object object = noAccess.newInstance();
Object object1 = canAccess.newInstance("小明");
System.out.println();
System.out.println(object.toString());
System.out.println(object1.toString());
}
}
结果:
使用Clone方法
如果要使用Clone方法,那就需要实现Cloneable接口并且需要重写clone()方法,调用这个对象的clone方法,将传入的对象内容全部拷贝,然后java虚拟机会给程序创建一个新的对象,在这个过程中,程序不会调用拷贝类的任何构造函数。 实例代码1:
class Test implements Cloneable{
private String name;
private Test() {
System.out.println("无参数构造方法");
}
public Test(String name) {
System.out.println("有参数构造方法");
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 重写clone方法
public Test clone() throws CloneNotSupportedException {
return (Test)super.clone();
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Test test = new Test("123");
Test test1 = test.clone();
System.out.println(test.toString());
System.out.println(test1.toString());
// false
System.out.println(test == test1);
// false
System.out.println(test.equals(test1));
}
}
结果:
调用clone方法创建出来的对象是一个全新的对象,调用
==
或者 equals()
方法判断都可以看出。但是假如说Test
类里面存在有一个内部类,那么调用clone
方法创建出来的对象,外部类对象是一个全新的对象,内部类的对象引用指向的是内存中的同一块区域,也就是说内部类调用clone方法达到的效果是创建了一个新的对象引用,并没有发生内存分配等操作。
实例代码2:
class Sky{
private String type;
public Sky() {
}
public Sky(String type) {
this.type = type;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
class Test implements Cloneable{
private String name;
private Sky sky;
private Test() {
System.out.println("无参数构造方法");
}
public Test(String name, Sky sky) {
this.name = name;
this.sky = sky;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Sky getSky() {
return sky;
}
public void setSky(Sky sky) {
this.sky = sky;
}
// 重写clone方法
public Test clone() throws CloneNotSupportedException {
return (Test)super.clone();
}
}
public class Demo {
public static void main(String[] args) throws Exception {
Sky sky = new Sky("蓝蓝的");
Test test = new Test("小明", sky);
Test test1 = test.clone();
System.out.println("测试Test外部类");
System.out.println(test);
System.out.println(test1);
System.out.println("测试内部类");
System.out.println(test.getSky());
System.out.println(test1.getSky());
System.out.println("测试 == 比较");
System.out.println(test == test1);
System.out.println(test.getSky() == test1.getSky());
System.out.println("测试equals方法");
System.out.println(test.equals(test1));
System.out.println(test.getSky().equals(test1.getSky()));
}
}
结果:
使用序列化和反序列化
什么情况下使用序列化和反序列化?
1、如果想要把内存中的对象状态保存到一个文件或者是数据库中的时候。 2、如果想使用套接字在网络上传输类对象的时候。 3、如果想通过RMI传输对象的时候。
序列化: 将一个类对象通过流的方式存储到文件中,注意:这个对象要重写Serializable 接口才能被序列化。另外实现序列化需要注意三点:
1、如果没有实现Serializable接口,会出现NotSerializableException 2、要求对象中的所有属性也都是可以序列化 3、如果某个属性不想序列化,可以在属性上加
transient
关键字
反序列化: 将字节内容还原出来,转换成java类对象
序列化可以实现一个类的持久化,将一个类进行编码存储在本地或者可以通过网络传输。通过反序列化获取一个对象,类需要实现Serializable
接口,当使用反序列化时,java虚拟机会创建一个新的对象,这个过程中,java虚拟机不会调用任何的构造函数。
序列化代码工具类:
public class SerializationUtils {
public static void writeObject(Serializable serializable, String name){
try{
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(name));
out.writeObject(serializable);
out.close();
}catch (Exception e){
System.out.println("序列化出现异常!");
e.printStackTrace();
}
}
public static Object readObject(String name){
Object object = null;
try {
ObjectInputStream input = new ObjectInputStream(new FileInputStream(name));
object = input.readObject();
input.close();
}catch (Exception e){
System.out.println("反序列化出现异常!");
e.printStackTrace();
}
return object;
}
}
测试类:
import java.io.Serializable;
class Test implements Serializable {
private String name;
public Test() {
}
public Test(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Demo{
public static void main(String[] args) throws Exception {
Test test = new Test("小华");
// 路径名自定义。
SerializationUtils.writeObject(test, "路径名\\test.txt");
Test test1 = (Test) SerializationUtils.readObject("路径名\\test.txt");
System.out.println(test.getName());
System.out.println(test1.getName());
System.out.println("测试输出:");
System.out.println(test);
System.out.println(test1);
System.out.println("测试 == 比较");
System.out.println(test == test1);
System.out.println("测试equals方法");
System.out.println(test.equals(test1));
}
}
结果:
获取Class对象
使用反射机制可以动态获取类对象信息,Class类对象可以看作是一个工具,通过这个工具可以获取类的方法和属性,然后获取到的信息就可以提供给运行的程序。
直接获取
如果一开始就知道需要加载的是一个具体的类,那么可以直接通过以下这个方法获取类对象信息。
实例代码:
public class Demo {
public Demo(){
}
public Demo(int a){
}
public void test(){
int a = 100;
a ++;
System.out.println("a的值为: " + a);
}
public static void main(String[] args) throws Exception {
Class classz = Demo.class;
Demo demo = (Demo) classz.newInstance();
demo.test();
}
}
结果:
通过Class.forname()获取
一般来说,获取Class类对象是不知道具体类的,这个时候可以通过遍历包下面的类来获取Class对象,通过这个方式获取到的Class对象不会初始化类。 实例代码:
import java.io.Serializable;
import java.lang.reflect.Method;
class Test implements Serializable {
private String name;
public Test() {
System.out.println("无参数构造方法");
}
public Test(String name) {
System.out.println("有参数构造方法");
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Demo{
public static void main(String[] args) throws Exception {
Class classz = Class.forName("Concurrent.Test");
Method[] methods = classz.getDeclaredMethods();
for (java.lang.reflect.Method method: methods) {
System.out.println(method);
}
}
}
结果:
通过instance.getClass()获取
通过这个方法获取到Class类对象需要先初始化类实例。
Test test = new Test();
Class classz = test.getClass();
通过类加载器获取
通过类加载器获取到Class类对象,需要传入类路径获取。
Class clazz = ClassLoader.loadClass(“Concurrent.Test”);
通过这个方法获取到的Class类对象并不会进行初始化,这就说明静态代码块和静态对象这些都不会执行。
java的引用
在java对象的使用中,一共存在有四种引用中,每一种引用的作用和范围都不同,这其中牵涉到了垃圾回收,持久化等等。
强引用
这是最普遍的一种引用方式,比如说String str= “abcd”
,变量str就是对字符串“abcd”
的一种强引用,只要强引用存在,那么垃圾回收器就不会回收这个对象。
软引用
这种引用用于描述还有使用价值但是非必须的对象,如果内存足够,那就不会回收这个对象,如果内存不足够,则回收。一般用于实现内存敏感的高速缓存,软引用可以和引用队列ReferenceQueue
一起使用。如果软引用的对象被垃圾回收,java虚拟机就会把这个软引用加入到与之关联的引用对列中。
弱引用
弱引用的用法和软引用大致相同,它们二者的区别:只是具有弱引用的对象具有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管内存是否足够,垃圾回收器都会回收这个对象。
虚引用
字面上理解的意思就是形同虚设,它和其他的几种引用都不太一样,虚引用不会决定对象的生命周期,如果一个对象只是具有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须要和引用队列ReferenceQueue
联合在一起使用,当垃圾回收器准备回收一个对象的时候,如果发现它还具有虚引用,那么在对象被回收之前就会把这个虚引用添加到与之对应的引用对列中。
深拷贝和浅拷贝
深拷贝和浅拷贝的主要区别是拷贝的程度不同,深拷贝是把类的所有信息全部拷贝并重新分配内存,浅拷贝只是拷贝当前类对象信息,而不拷贝当前类对象里面所包含的其他类对象引用信息
深拷贝
深拷贝是将一整个对象信息进行拷贝,它会拷贝对象的所有信息,比如说类的成员变量,方法和类的对象引用等等,并且拷贝的过程中还会动态分配内存,简单来说深拷贝是把对象引用和对象一起拷贝,深拷贝相对来说比浅拷贝速度慢,而且花销大。
实例代码:
class Student implements Cloneable{
private String name;
private int age;
private Teacher teacher;
public Student() {
}
public Student(String name, int age, Teacher teacher) {
this.name = name;
this.age = age;
this.teacher = teacher;
}
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 Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public Object clone() throws CloneNotSupportedException {
// 深拷贝
Student student = (Student) super.clone();
student.setTeacher((Teacher) student.getTeacher().clone());
return student;
}
}
class Teacher implements Cloneable{
private String name;
private int age;
public Teacher() {
}
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
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 Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CopyTest {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("春华", 32);
Student student = new Student("小华", 16, teacher);
Student student1 = (Student) student.clone();
System.out.println("测试外部类:");
System.out.println(student);
System.out.println(student1);
System.out.println("测试内部类");
System.out.println(student.getTeacher());
System.out.println(student1.getTeacher());
System.out.println("修改Teacher信息");
teacher.setName("夏眠");
teacher.setAge(45);
System.out.println("拷贝前姓名:" + student.getTeacher().getName() + " 年龄:" + student.getTeacher().getAge());
System.out.println("拷贝后姓名:" + student1.getTeacher().getName() + " 年龄:" + student1.getTeacher().getAge());
}
}
结果:
浅拷贝
浅拷贝中被复制的对象的所有变量都含有与原来对象相同的值,但是所有的原来对其他对象的引用仍然指向原来的对象,简单来说就是浅拷贝会对“主对象”
外部类对象进行拷贝并且会动态分配内存,但是不会复制“主对象”
里面的对象,仅仅只是复制一个内部类对象引用的副本,它的内存空间两者共享。(主对象可以看作是具有public修饰的唯一的类创建的对象)
实例代码:
class Student implements Cloneable{
private String name;
private int age;
private Teacher teacher;
public Student() {
}
public Student(String name, int age, Teacher teacher) {
this.name = name;
this.age = age;
this.teacher = teacher;
}
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 Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public Object clone() throws CloneNotSupportedException {
// 浅拷贝
Object object = super.clone();
return object;
}
}
class Teacher implements Cloneable{
private String name;
private int age;
public Teacher() {
}
public Teacher(String name, int age) {
this.name = name;
this.age = age;
}
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 class CopyTest {
public static void main(String[] args) throws CloneNotSupportedException {
Teacher teacher = new Teacher("春华", 32);
Student student = new Student("小华", 16, teacher);
Student student1 = (Student) student.clone();
System.out.println("测试外部类:");
System.out.println(student);
System.out.println(student1);
System.out.println("测试内部类");
System.out.println(student.getTeacher());
System.out.println(student1.getTeacher());
System.out.println("修改Teacher信息");
teacher.setName("夏眠");
teacher.setAge(45);
System.out.println("拷贝前姓名:" + student.getTeacher().getName() + " 年龄:" + student.getTeacher().getAge());
System.out.println("拷贝后姓名:" + student1.getTeacher().getName() + " 年龄:" + student1.getTeacher().getAge());
System.out.println("修改Student信息");
student.setName("马源");
student.setAge(10);
System.out.println("拷贝前姓名:" + student.getName() + " 年龄:" + student.getAge());
System.out.println("拷贝后姓名:" + student1.getName() + " 年龄:" + student1.getAge());
}
}
结果:
接口和抽象类
接口(interface)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。 接口中的方法定义默认为public abstract
类型,接口中的成员变量类型默认为public static final
。另外,接口和抽象类在方法上
有区别:
-
抽象类可以有构造方法,接口中不能有构造方法。
-
抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。
-
抽象类中可以有普通成员变量,接口中没有普通成员变量
-
抽象类中的抽象方法的访问类型可以是public,protected和默认类型
-
抽象类中可以包含静态方法,接口中不能包含静态方法
-
抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型
-
一个类可以实现多个接口,但只能继承一个抽象类。二者在应用方面也有一定的区别:接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用,例如,模板方法设计模式是抽象类的一个典型应用,假设某个项目的所有Servlet类都要用相同的方式进行权限判断、记录访问日志和处理异常,那么就可以定义一个抽象的基类,让所有的Servlet都继承这个抽象基类,在抽象基类的service方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中只是完成各自的业务逻辑代码。
接口和抽象类的区别
接口和抽象类的主要区别有以下几种: 实现方式不同: 抽象类需要使用extends来继承,子类可以选择重写抽象类里面的非抽象方法,但是必须要重写抽象类里面的抽象方法(使用abstract修饰的方法)。接口必须要使用implement来实现。 构造函数: 抽象类可以有构造函数,但是接口不可以有构造函数。 实现的数量: 一个类可以有很多个接口,但是只能继承一个抽象类。 访问修饰符不同: 接口中的方法默认使用public修饰,抽象类中的方法可以使用任意的访问修饰符。 实例化: 接口和抽象类均不能实例化,但是可以采用类似于父类和之类的那种向上转型的方法进行实例话,即接口或者抽象类当做对象引用,创建的对象实例为继承了抽象类或者实现了接口的类。
this和super
super
1、它引用当前父类的直接成员(用来访问直接父类中被隐藏的父类的成员数据或是函数,基类与派生类中有相同的成员定义时如:super.变量名
,super.成员函数据名(实参)
)会直接调用父类的成员。
2、super()
和this()
类似,区别是super()
在子类中调用的父类的构造方法,this()
在本类中调用类的其他构造方法。
3、super()和this()
都需要放在构造函数的第一行。
4、this()和super()
都是指向对象,因此它们不可以放在静态(static)
环境中使用,包括static变量,static方法,static代码块。
this
1、它代表的是当前对象名
(如果在类中容易产生歧义的,建议使用this指向当前对象用以区分,比如说get方法和set方法
,因为函数的形参和类的成员名同名,需要this来指明当前对象成员变量名)
2、this
可以调用构造器,但是不可以调用两个构造器
3、this
不可以和super
同时出现在一个构造函数里,因为this会调用其他的构造函数,其他的构造函数会有super
的存在,所以在一个构造函数里面出现了相同的语句,那就失去了语义,编译器也不允许通过。
4、从本质上讲,this是一个指向当前类对象的指针,而super是一个java的关键字。
final、finally、finalize的区别
final用法
final是一个修饰符(关键字)
有三种用法:如果一个类
使用了final
修饰,那么这个类不可以再被其他类派生
,即不可以被继承,可以理解为final与abstract为反义词。
如果使用final
修饰了变量,那么可以确保的是这个变量在使用的过程中不被改变,另外在声明的初始时候就必须要给变量初始化值。如果使用final修饰方法,这个方法不可以被派生类重写,只有使用权限。
finally用法
通常放在try......catch......的后面构造总是执行的代码块,这就意味着程序无论正常执行还是发生异常故障,finally的代码块只要java虚拟机不关闭都可以执行,一般来说把释放系统资源的代码放在finally的代码块。
实例代码:
public class Demo {
public void test(){
int a = 100;
try{
System.out.println("a的值为: " + a);
a ++;
throw new Exception("产生异常");
}catch (Exception e){
System.out.println("catch代码块, a的值为: " + a);
a ++;
// return;
}finally {
System.out.println("finally代码块, a的值为: " + a);
return;
}
}
public static void main(String[] args){
Demo demo = new Demo();
demo.test();
}
}
结果:
修改异常代码块:
try{
System.out.println("a的值为: " + a / 0);
return;
}catch (Exception e){
System.out.println("catch代码块, a的值为: " + a);
a ++;
}finally {
System.out.println("finally代码块, a的值为: " + a);
}
结果:
首先try代码块中有两个操作,第一个操作是先执行 a 除以 0,由于a 为整数,除数不可以为零,产生异常,不执行打印代码,然后被异常捕获,接着执行
finally
代码块。如果将try
代码块中的return;
放到catch
代码块中,结果一致,没有任何改变。如果finally代码块中也包含了return;
,那么finally
代码块中的return;
会覆盖掉前面的return;
。
finalize用法
finalize是Object类里面定义的方法,java允许使用这个方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作,这个方法是有垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以整理系统资源或者执行其他的清理工作。
静态变量和实例变量的区别
静态变量是被static修饰符修饰的变量,也可以称为类变量,它是属于类的,而不是属于类的对象。一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝(存档)。实例变量必须要依存在某一特定的实例,需要先创建对象然后才能通过对象访问到它,静态变量可以使多个对象共享内存,因此在上下文类或者是工具类中会存在有大量的静态成员。
静态成员是否可以使用非静态成员
静态成员(静态变量、静态方法)是不可以使用非静态成员(实例变量、实例方法)的。因为非静态成员的使用首先需要创建对象,但是在调用静态成员的时候,类并没有被初始化,这个时候不存在有类实例,无法调用非静态成员。
== 与equals的区别
== 比较
如果比较的对象是基本数据类型,则比较的是数值是否相等,如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
equals比较
equals方法用来比较两个对象的内容是否相等,需要注意的是equals方法不可以用来比较基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。实际上很多类都对equals方法进行了重写,比如说String类和Integer类,在这些类中实现的equals方法比较的是值相等,可以看作一般情况下比较的是值相等。
字符串类
字节与字符的区别
1、字节是存储容量的基本单位 2、字符是数字、字母、汉字以及其他语言的各种符号。 3、1个字节 = 8个二进制单位,一个字符由一个字节或多个字节的二进制单位组成。
不可变的String类
首先来看一下String类的底层数据结构。String类底层存储数据的变量使用了final修饰,final的用法上文也提及了,使用了final修饰的变量,在使用的过程中不允许修改。因此String类是不可变的。
/** The value is used for character storage. */
private final char value[];
String类常用的API方法:
indexOf():返回指定字符的索引。 charAt():返回指定索引处的字符。 replace():字符串替换。 trim():去除字符串两端空白。 split():分割字符串,返回一个分割后的字符串数组。 getBytes():返回字符串的 byte 类型数组。 length():返回字符串长度。 toLowerCase():将字符串转成小写字母。 toUpperCase():将字符串转成大写字符。 substring():截取字符串。 equals():字符串⽐比较。
String设计为不可变
的因素主要有以下三点
:
字符串常量池相关: 字符串常量池是java堆内存中一个特殊的存储区域,当程序需要创建一个String对象的时候,假如这个对象已经存在于字符串常量池中,那么就不会创建一个新的对象,而是引用已经存在的对象。
实例代码:
public class StringTest {
public static void main(String[] args) {
// 值存放在堆内存中
String str = new String("abcd");
// 值存放在常量池中
String str1 = "abcd";
System.out.println(str.hashCode());
System.out.println(str1.hashCode());
// 比较引用地址(一个创建在常量池中,一个创建在堆内存中)
System.out.println(str == str1);
// 比较值相等
System.out.println(str.equals(str1));
}
}
结果:
允许String对象缓存hashCode: Java中
String
对象的哈希码被频繁的使用,在一些常见的集合容器比如HashMap
容器中,String对象的不变性保持了hashCode
的唯一性,因此可以放心地进行缓存,这可以看作是一种性能优化的手段,因为在使用HashMap
等集合中,不必须每次都重新计算hashCode
的值。
String类被许多java的类库当做参数: 比如说网络连接地址URL
,文件路径Path
,还有反射机制所需要的String
参数,如果说String
类是可变的,那将会引发很多安全隐患。
注意: 在这里插入一个String类特殊的intern()
方法的使用,String中的intern()
方法是一个Native
方法,它首先会从常量池中查找是否存在该常量值的字符串,如果不存在,那就先在常量池中创建,否则会返回常量池中已经存在了的字符串引用。
实例代码:
public class StringTest {
public static void main(String[] args) {
String str = "abcd";
String str1 = "abcd".intern();
System.out.println(str.hashCode());
System.out.println(str1.hashCode());
System.out.println(str == str1);
System.out.println(str.equals(str1));
}
}
结果:
三者区别
String类: 用于字符串操作,属于不可变类,(String类不是基本的数据类型,是引用类型,它的底层是使用char数组实现的)。 StringBuilder类: 用法与StringBuffer类似,都是字符串缓存区,但是属于线程不安全的实现类。 StringBuffer类: 用于字符串操作,和StringBuilder实现类不同的是,它实现了线程安全,对方法加了同步锁。需要注意的一点:不是所有的StringBuffer类的都添加了同步锁。
执行效率:StringBuilder > StringBuffer > String
常见的关于字符串的面试题
String类实现修改的原理
如果使用String类来保存字符串数据,当需要修改的时候,其修改的过程是这样子的:首先创建一个StringBuffer
,其次调用StringBuffer
的append()
方法,最后调用StringBuffer
类的toString()
方法返回一个新的String
类对象。
String类字符串的两种初始化方法
String类的初始化共有两种方法,一种是创建String类的引用指向使用双引号包含了字符串,另外一种是使用new关键字创建一个新的String类对象。这两种方法有很大的区别。
实例代码:
public class StringTest {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str3 == str4); // false
System.out.println(str3.equals(str4)); // true
}
}
结果:
String类初始化指向使用双引号包裹的字符串,这个时候创建的字符串对象存储在字符串常量池里面,使用new关键字创建的新的String类对象是一个全新的对象,存储在堆内存里面,每次使用new关键字,JVM都会在堆内存中划分一块新的地址空间存储新的字符串。
形参与实参
方法的定义可能会用到参数(有参的方法)
,参数在程序语言中分为:
实参(实际参数) :用于传递给函数/方法的参数,必须有确定的值。 形参(形式参数) :用于定义函数/方法,接收实参,不需要有确定的值。
import java.lang.*;
public class Main{
public static int add(int a, int b){
return a+b;
}
public static void main(String[] args){
int n = 10, m = 100;
int sum = add(n,m);
System.out.println(sum);
}
}
在上述代码中,n,m是实际参数,而方法中的 a,b则是形式参数。方法返回a和b的和,传入10和100,所以返回110。
值传递和引用传递
程序设计语言将实参传递给方法(或函数)的方式分为两种: 值传递 : 方法接收的是实参值的拷贝,会创建副本。 引用传递 : 方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。 在java语言中,只有值传递,没有引用传递。 实例代码1:
import java.lang.*;
public class Main{
public static void swap(String str1, String str2){
System.out.println("swap-first: ");
System.out.println(str1);
System.out.println(str2);
String str = str1;
str1 = str2;
str2 = str;
System.out.println("swap-finish: ");
System.out.println(str1);
System.out.println(str2);
}
public static void main(String[] args){
String str1 = "abcd";
String str2 = "efgh";
swap(str1, str2);
System.out.println("main - println: ");
System.out.println(str1);
System.out.println(str2);
}
}
结果:
可以看出str1和str2在传入swap()方法时,确实是交换成功了,因为两个变量的指向地址在前后发生了变化,但是当swap()函数执行完毕返回main函数的时候,这个时候指向str1和str2的又变回了原来的位置,从这里可以看出,传入给swap()方法的是两个变量的副本。所以swap()方法对str1和str2两个变量指向的值做任何操作都不会影响到main函数中这两个变量的值。
实例代码2:
import java.lang.*;
public class Main{
public static void swap(int[] str){
System.out.println("swap-first: ");
System.out.println(str[0] + " " + str[1]);
int temp = str[0];
str[0] = str[1];
str[1] = temp;
System.out.println("swap-finish: ");
System.out.println(str[0] + " " + str[1]);
}
public static void main(String[] args){
int[] str = new int[] {1,2};
swap(str);
System.out.println("main-println: ");
System.out.println(str[0] + " " + str[1]);
}
}
结果:
在这里,由于swap传入的是数组的地址(拷贝的是str(实参)地址),而且两者指向内存中同一个数组对象,所以,在swap方法中对数组做出了更改,会影响到main函数中的数组。因为数组的方法靠的是地址值的指向。
实例代码3:
package AIgorithmTest;
import java.lang.*;
class Student{
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Main{
public static void swap(Student str1, Student str2){
System.out.println("swap-first: ");
System.out.println("str1: " + str1.toString());
System.out.println("str2: " + str2.toString());
Student str = str1;
str1 = str2;
str2 = str;
System.out.println("swap-finish: ");
System.out.println("str1: " + str1.toString());
System.out.println("str2: " + str2.toString());
}
public static void main(String[] args){
Student str1 = new Student("小红",32);
Student str2 = new Student("小明", 100);
swap(str1, str2);
System.out.println("main-println: ");
System.out.println("str1: " + str1.toString());
System.out.println("str2: " + str2.toString());
}
}
结果:
在第二个例子中已经说明了,如果是引用类型的变量,那就会拷贝引用变量的地址值,在方法中操作的是拷贝后的地址值,所以swap()方法实际上交换的是拷贝后的两个引用地址值,对与原来的引用类型的变量的地址值无法做出更改。
一句话总结:如果传入方法中的是基本的数据类型,那就会拷贝基本数据类型的变量(创建副本),如果传入方法的是引用类型,那就会拷贝引用
java11中String类,StringBuffer类和StringBuilder类区别
String类
基础
所属包:
package java.lang;
继承和实现关系
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}
它实现了Serializable, Comparable, CharSequence接口,类被final关键字修饰,所以类不能不能被继承,并且是线程安全的。
实现的类图如下:
成员变量
// 该值用于字符存储
@Stable
// 源码为java11的,在java8中,String的底层是字符数组: private final char value[];
private final byte[] value;
// 用于对字节进行编码的编码标识符
private final byte coder;
// 缓存字符串的哈希码
private int hash;
// 使用 JDK 1.0.2 中的 serialVersionUID 以实现互操作性
private static final long serialVersionUID = -6849794470754667710L;
// 如果禁用字符串压缩,则 code value 中的字节始终以 UTF16 编码
static final boolean COMPACT_STRINGS;
// 默认是true
static {
COMPACT_STRINGS = true;
}
//用于忽略大小写得比较两个字符串。
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
构造方法
无参构造
public String() {
this.value = "".value;
this.coder = "".coder;
}
有参构造: 参数为String字符串
@HotSpotIntrinsicCandidate
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
有参构造: 参数为字符数组,封装私有构造函数,将 char[] 值存储到每个字节表示的 byte[] 中对应字符的 8 个低位。
public String(char value[]) {
this(value, 0, value.length, null);
}
// this指针调用方法
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len)
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
有参构造方法: 参数为字符数组,偏移值,和长度,分配一个新的String,其中包含来自字符数组参数的子数组的字符。offset参数是子数组第一个字符的索引,count 参数指定子数组的长度。子数组的内容被复制;随后对字符数组的修改不会影响新创建的字符串。
public String(char value[], int offset, int count) {
this(value, offset, count, rangeCheck(value, offset, count));
}
private static Void rangeCheck(char[] value, int offset, int count) {
checkBoundsOffCount(offset, count, value.length);
return null;
}
static void checkBoundsOffCount(int offset, int count, int length) {
if (offset < 0 || count < 0 || offset > length - count) {
throw new StringIndexOutOfBoundsException(
"offset " + offset + ", count " + count + ", length " + length);
}
}
String类中常用方法
length: 返回此字符串的长度。长度等于字符串中Unicode 代码单元的数量。
public int length() {
return value.length >> coder();
}
isEmpty: 长度为0,判断当前字符串是否为空
public boolean isEmpty() {
return value.length == 0;
}
charAt: 返回指定索引处的char值。索引范围从0 到 length() - 1。序列的第一个 char值在索引 0 处,下一个在索引 1 处,依此类推,与数组索引一样。
public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
getByte: 获取字节数,使用命名字符集将此 String 编码为字节序列,将结果存储到新的字节数组中。未指定此字符串无法在给定字符集中编码时此方法的行为。
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, coder(), value);
}
public byte[] getBytes(Charset charset) {
if (charset == null) throw new NullPointerException();
return StringCoding.encode(charset, coder(), value);
}
public byte[] getBytes() {
return StringCoding.encode(coder(), value);
}
equal: 判断是否相等,将此字符串与指定对象进行比较。结果是true当且仅当参数不是 null 并且是一个String 对象,表示与此对象相同的字符序列。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
compareTo: 按字典顺序比较两个字符串。比较基于字符串中每个字符的 Unicode 值。此 String对象表示的字符序列按字典顺序与参数字符串表示的字符序列进行比较。如果此String对象按字典顺序位于参数字符串之前,则结果为负整数。如果此String对象按字典顺序跟随参数字符串,则结果为正整数。如果字符串相等,则结果为零;当 equals(Object)方法返回true时,compareTo返回 code 0。
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
startsWith: 测试此字符串是否以指定的前缀开头。
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
public boolean startsWith(String prefix, int toffset) {
// Note: toffset might be near -1>>>1.
if (toffset < 0 || toffset > length() - prefix.length()) {
return false;
}
byte ta[] = value;
byte pa[] = prefix.value;
int po = 0;
int pc = pa.length;
if (coder() == prefix.coder()) {
int to = isLatin1() ? toffset : toffset << 1;
while (po < pc) {
if (ta[to++] != pa[po++]) {
return false;
}
}
} else {
if (isLatin1()) { // && pcoder == UTF16
return false;
}
// coder == UTF16 && pcoder == LATIN1)
while (po < pc) {
if (StringUTF16.getChar(ta, toffset++) != (pa[po++] & 0xff)) {
return false;
}
}
}
return true;
}
// 测试此字符串是否以指定的后缀结尾。
public boolean endsWith(String suffix) {
return startsWith(suffix, length() - suffix.length());
}
hashCode: 返回此字符串的哈希码。String 对象的哈希码计算为s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
indexOf(): 判断字符从0开始位于字符串的哪个位置
public int indexOf(int ch) {
return indexOf(ch, 0);
}
public int indexOf(int ch, int fromIndex) {
return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex)
: StringUTF16.indexOf(value, ch, fromIndex);
}
lastIndexOf(): 判断字符从字符串尾部开始位于字符串的哪个位置
public int lastIndexOf(int ch) {
return lastIndexOf(ch, length() - 1);
}
public int lastIndexOf(int ch, int fromIndex) {
return isLatin1() ? StringLatin1.lastIndexOf(value, ch, fromIndex)
: StringUTF16.lastIndexOf(value, ch, fromIndex);
}
substring: 截取字符串中指定位置的字符
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = length() - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
if (beginIndex == 0) {
return this;
}
return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
: StringUTF16.newString(value, beginIndex, subLen);
}
public String substring(int beginIndex, int endIndex) {
int length = length();
checkBoundsBeginEnd(beginIndex, endIndex, length);
int subLen = endIndex - beginIndex;
if (beginIndex == 0 && endIndex == length) {
return this;
}
return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
: StringUTF16.newString(value, beginIndex, subLen);
}
concat(): 拼接字符串
public String concat(String str) {
if (str.isEmpty()) {
return this;
}
if (coder() == str.coder()) {
byte[] val = this.value;
byte[] oval = str.value;
int len = val.length + oval.length;
byte[] buf = Arrays.copyOf(val, len);
System.arraycopy(oval, 0, buf, val.length, oval.length);
return new String(buf, coder);
}
int len = length();
int olen = str.length();
byte[] buf = StringUTF16.newBytesFor(len + olen);
getBytes(buf, 0, UTF16);
str.getBytes(buf, len, UTF16);
return new String(buf, UTF16);
}
replace(): 将源字符替换成目标字符
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar)
: StringUTF16.replace(value, oldChar, newChar);
if (ret != null) {
return ret;
}
}
return this;
}
// 用指定的文字替换序列替换此字符串中与文字目标序列匹配的每个子字符串。
// 替换从字符串的开头到结尾进行,例如,将字符串“aaa”中的“aa”替换为“b”将导致“ba”而不是“ab”。
public String replace(CharSequence target, CharSequence replacement) {
String tgtStr = target.toString();
String replStr = replacement.toString();
int j = indexOf(tgtStr);
if (j < 0) {
return this;
}
int tgtLen = tgtStr.length();
int tgtLen1 = Math.max(tgtLen, 1);
int thisLen = length();
int newLenHint = thisLen - tgtLen + replStr.length();
if (newLenHint < 0) {
throw new OutOfMemoryError();
}
StringBuilder sb = new StringBuilder(newLenHint);
int i = 0;
do {
sb.append(this, i, j).append(replStr);
i = j + tgtLen;
} while (j < thisLen && (j = indexOf(tgtStr, j + tgtLen1)) > 0);
return sb.append(this, i, thisLen).toString();
}
// 使用正则匹配并替换所有的字符
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
matches: 判断这个字符串是否匹配给定的正则表达式
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
contains: 当且仅当此字符串包含指定的 char 值序列时才返回 true。
public boolean contains(CharSequence s) {
return indexOf(s.toString()) >= 0;
}
spilt: 将一个字符串按照正则表达式进行分割,分割成多个字符串并组成一个数组
public String[] split(String regex) {
return split(regex, 0);
}
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.length() == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
int last = length();
list.add(substring(off, last));
off = last;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, length()));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).isEmpty()) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
区别
汇总
String类是不可变类,当一个String对象被创建,则包含在对象中的字符序列是不可改变的,直至对象被销毁;StringBuffer对象代表可变字符串对象,且线程安全;StringBuilder类代表可变字符串对象,且非线程安全
区别一
String是final类不能被继承且为字符串常量,而StringBuilder和StringBuffer均为字符串变量。
在Java中字符串使用String类进行表示,但是String类表示字符串有一个最大的问题:“字符串常量一旦声明则不可改变,而字符串对象可以改变,但是改变的是其内存地址的指向。”所以String类不适合于频繁修改的字符串操作上,所以在这种情况下,往往可以使用StringBuffer类,即StringBuffer类方便用户进行内容修改。
public static void main(String[] args) {
String str="123";
System.out.print("String字符串常量str:"+str+"的初始地址是:");
System.out.println(str.hashCode());
str=str+"456";
System.out.print("String字符串常量str:"+str+"赋值后的地址是:");
System.out.println(str.hashCode());
StringBuffer sb=new StringBuffer().append("123");
System.out.print("StringBuffer字符串变量sb:"+sb+"的初始地址是:");
System.out.println(sb.hashCode());
sb.append("456");
System.out.print("StringBuffer字符串变量sb:"+sb+"赋值后地址是:");
System.out.println(sb.hashCode());
StringBuilder sbd=new StringBuilder().append("123");
System.out.print("StringBuilder字符串变量sbd:"+sbd+"的初始地址是:");
System.out.println(sbd.hashCode());
sbd.append("456");
System.out.print("StringBuilder字符串变量sbd:"+sbd+"赋值后地址是:");
System.out.println(sbd.hashCode());
}
结果: String类型是字符串常量,当进行字符串操作时,地址即发生改变。StringBuffer和StringBuilder类型是字符串变量,当使用apend()等操作时,内存地址不发生变化
区别二:
类的定义不同,通过三个定义结构可以发现,String、StringBuffer和StringBuilder类都是CharSequence接口的子类,也就证明String、StringBuffer和StringBuilder类的对象都可以利用自动向上转型的操作为CharSequence接口实例化。 String类:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}
StringBuilder类:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
{
}
StringBuffer类:
public final class StringBuffer
extends AbstractStringBuilder
implements Serializable, Comparable<StringBuffer>, CharSequence
{
String类
String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。 实例代码:
public static void main(String[] args) {
String a = "abcd";
a = "123";
System.out.println(a);
}
结果:
不是说不可变吗,为什么还可以赋值成功呢?其实这是一个新的对象引用了。查看编译后的源代码就可以看懂了。或者说如下图:可以看出来,再次给a赋值时,并不是对原来堆中实例对象进行重新赋值,而是生成一个新的实例对象,并且指向“456”这个字符串,a则指向最新生成的实例对象,之前的实例对象仍然存在,如果没有被再次引用,则会被垃圾回收。
StringBuffer 类
StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。 实例代码:
public static void main(String[] args) {
StringBuffer sb=new StringBuffer().append("123");
System.out.print("StringBuffer字符串变量sb:"+sb+"的初始地址是:");
System.out.println(sb.hashCode());
sb.append("456");
System.out.print("StringBuffer字符串变量sb:"+sb+"赋值后地址是:");
System.out.println(sb.hashCode());
}
结果: 可以证明StringBuffer对象是一个字符序列可变的字符串,它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串。
StringBuilder类
StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。 StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。 实例代码:
public static void main(String[] args) {
StringBuilder sbd=new StringBuilder().append("123");
System.out.print("StringBuilder字符串变量sbd:"+sbd+"的初始地址是:");
System.out.println(sbd.hashCode());
sbd.append("456");
System.out.print("StringBuilder字符串变量sbd:"+sbd+"赋值后地址是:");
System.out.println(sbd.hashCode());
}
结果:
java8以后的版本改进
Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。在Java9以前字符串采用char[]数组来保存字符,因此字符串的每个字符占2字节;而Java9的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,因此字符串的每个字符只占1字节。所以Java9的字符串更加节省空间,字符串的功能方法也没有受到影响。
数组转换问题
假如这个时候有一个数组要求将数组转换成list类型的,那么问题来了:转换后的list类型中能使用什么方法呢? 实例代码:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
System.out.println(list);
list.add(5);
System.out.println(list.size());
list.remove(1);
System.out.println(list.size());
list.get(3);
}
先来猜猜,是不是都会正常输出,没有异常情况?如果是这样的话那就大错特错了,结果如下:
UnsupportedOperationException:是不支持的操作异常,也就是说,上面的几种方法有一种以上是不能正常操作的,那一个个排除
实例代码:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
System.out.println(list);
list.add(5);
// System.out.println(list.size());
// list.remove(1);
// System.out.println(list.size());
// list.get(3);
结果:
实例代码:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
System.out.println(list);
// list.add(5);
System.out.println(list.size());
// list.remove(1);
// System.out.println(list.size());
// System.out.println(list.get(2));
}
结果:
实例代码:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
System.out.println(list);
// list.add(5);
System.out.println(list.size());
list.remove(1);
// System.out.println(list.size());
// System.out.println(list.get(2));
}
结果:
实例代码:
public static void main(String[] args) {
List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
System.out.println(list);
// list.add(5);
System.out.println(list.size());
// list.remove(1);
// System.out.println(list.size());
System.out.println(list.get(2));
}
结果:
总结:只有get()方法和size()方法是可以正常使用的,其余的方法都不可以正常使用,那为什么呢?这就不得不查看底层代码了
当程序运行到
List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
调用的是Arrays类中的asList()方法:
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
这里又调用Arrays类中的构造方法,
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
在Arrays类中存在有ArrayList的静态内部类:
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public int size() {
return a.length;
}
@Override
public Object[] toArray() {
return a.clone();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
@Override
public int indexOf(Object o) {
E[] a = this.a;
if (o == null) {
for (int i = 0; i < a.length; i++)
if (a[i] == null)
return i;
} else {
for (int i = 0; i < a.length; i++)
if (o.equals(a[i]))
return i;
}
return -1;
}
@Override
public boolean contains(Object o) {
return indexOf(o) != -1;
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(a, Spliterator.ORDERED);
}
@Override
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : a) {
action.accept(e);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
E[] a = this.a;
for (int i = 0; i < a.length; i++) {
a[i] = operator.apply(a[i]);
}
}
@Override
public void sort(Comparator<? super E> c) {
Arrays.sort(a, c);
}
}
从这里不难看出,当程序调用Arrays类的内部类ArrayList的构造方法并返回一个list类型时,就只能使用ArrayList类中的方法了,除此之外不可以使用其他的方法。可以使用的方法有构造方法,toArray()方法,get()和set()方法,indexOf,contains,spliterator,forEach,replaceAll,sort和size()等方法。
反射基础
首先来看一下最常规的创建对象的方式:
ObjectClass clazz = new ObjectClass();
当程序执行到new ObjectClass的时候,java虚拟机会加载ObjectClass.class文件,这个文件是由ObjectClass.java生成的,当java虚拟机将ObjectClass.class加载进内存后,内存中会存在一个class对象,这就是一个类加载变成对象的大致过程,具体详细过程可以参考一下这篇文章。
知道了这些,那什么才是反射呢?具体来说,在运行状态时,JVM中构造任何一个类的对象,反射都可以获取到任意一个对象所属的类信息,以及这个类的成员变量或者方法,并且能够调用任意一个对象的属性或者方法。因此java中的反射机制理解为java语言具备了动态加载对象以及对对象的基本信息进行剖析和使用的能力。
反射提供的功能包括:
- 在运行时判断一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时获取一个类定义的成员变量以及方法
- 在运行时调用任意一个对象的方法
- 生成动态代理
Class类详解
Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class ans enum)或接口(interface and annotation)(每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取class对象)。数组同样也被映射为class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为 class 对象。
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
private static final int ANNOTATION= 0x00002000;
private static final int ENUM = 0x00004000;
private static final int SYNTHETIC = 0x00001000;
private static native void registerNatives();
static {
registerNatives();
}
/*
* Private constructor. Only the Java Virtual Machine creates Class objects. //私有构造器,只有JVM才能调用创建Class对象
* This constructor is not used and prevents the default constructor being
* generated.
*/
private Class(ClassLoader loader) {
// Initialize final field for classLoader. The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null.
classLoader = loader;
}
因此我们可以得出以下几种结论:
- Class类也是类的一种,与class关键字是不一样的。
- 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件) 。
- 每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
- Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载
- Class类的对象作用是运行时提供或获得某个对象的类型信息。
类加载
类的加载器
ava虚拟机的类加载器一共有三种,分别是启动类加载器(引导类加载器(根类加载器):Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(系统类加载器 AppClassLoader)。还有就是在java程序中,我们可以自行定义一个类加载器,这个类加载器被划分为自定义类加载器。详情请参考这篇文章
类的加载机制
类的加载机制一共有四种,分别是全盘负责机制,父类委托机制,缓存机制,还有最重要的双亲委派机制。详情请参考这篇文章
反射的使用场景
获取对象的包名以及类名
package FleshTest;
public class MyInvocation {
public static void main(String[] args) {
getClassNameTest();
}
public static void getClassNameTest(){
MyInvocation myInvocation = new MyInvocation();
System.out.println("class: " + myInvocation.getClass());
System.out.println("simpleName: " + myInvocation.getClass().getSimpleName());
System.out.println("name: " + myInvocation.getClass().getName());
System.out.println("package: " +
"" + myInvocation.getClass().getPackage());
}
}
结果:
1、getClass():打印会带着class+全类名
2、getClass().getSimpleName():只会打印出类名
3、getName():会打印全类名
4、getClass().getPackage():打印出package+包名
getClass()获取到的是一个对象,getPackage()也是。
Class类对象的获取
在java中,一切皆对象。java中可以分为两种对象,实例对象和Class对象。这里我们说的获取Class对象,其实就是第二种,Class对象代表的是每个类在运行时的类型信息,指和类相关的信息。比如有一个Student类,我们用Student student = new Student(),new一个对象出来,这个时候Student这个类的信息其实就是存放在一个对象中,这个对象就是Class类的对象,而student这个实例对象也会和Class对象关联起来。
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {}
@Retention(value=RetentionPolicy.RUNTIME)
@interface Annos {
Anno[] value();
}
// 使用四个注解修饰该类
@SuppressWarnings(value="unchecked")
@Deprecated
// 使用重复注解修饰该类
@Anno
@Anno
public class ClassTest
{
// 定义一个私有构造器
private ClassTest()
{
}
// 定义一个有参数构造器
public ClassTest(String name)
{
System.out.println("执行有参数构造器");
}
// 定义一个无参数的info方法
public void info()
{
System.out.println("ִ执行无参数的info方法");
}
// 定义一个有参数的info方法
public void info(String str)
{
System.out.println("执行有参数的info方法"
+ "str值" + str);
}
// 定义一个测试用的内部类
class Inner
{
}
public static void main(String[] args)
throws Exception
{
// 下面代码可以获取 ClassTest 对应的 Class
Class<ClassTest> clazz = ClassTest.class;
//获取该 Class 对象所对应类的全部构造器
Constructor[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest 的全部构造器如下 : ");
for (Constructor c : ctors)
{
System.out.println(c);
}
//获取该 Class 对象所对应类的全部 public 构造器
Constructor[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest 的全部 public 构造器如下:");
for (Constructor c : publicCtors)
{
System.out.println(c);
}
// ClassTest 的全部 public 构造器如下:
Method[] mtds = clazz.getMethods();
System.out.println(" ClassTest 的全部 public 方法如下: ");
for (Method md : mtds)
{
System.out.println(md);
}
// 获取该 Class 对象所对应类的指定方法
System.out.println("ClassTest 里带一个字符串参数的 info 方法为:"
+ clazz.getMethod("info" , String.class));
// 获取该 Class 对象所对应类的全部注解
Annotation[] anns = clazz.getAnnotations();
System.out.println("获取该 Class 对象所对应类的全部注解");
for (Annotation an : anns)
{
System.out.println(an);
}
System.out.println(" 该 Class 元素上自的甘 @S归uppressWarr川 n 呵9 归s 注解为:"
+ Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
System.out.println("该 Class:元素上的@Anno 注解为 : "
+ Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
// 该 Class :7ë素上的 @Anno 注解为 :
Class<?>[] inners = clazz.getDeclaredClasses();
System.out.println("ClassTest 的全部内部类如下 : ");
for (Class c : inners)
{
System.out.println(c);
}
// 使用 Class . forName() 方法加载 ClassTest 的 Inner 内部类
Class inClazz = Class.forName("ClassTest$Inner");
// 通过 getDeclaringClass() 访问该类所在的外部类
System.out.println("inClazz 对应类的外部类为 :" +
inClazz.getDeclaringClass());
System.out.println("inClazz 对应类的外部类为 :" + clazz.getPackage());
System.out.println("ClassTest 的父类为 :" + clazz.getSuperclass());
}
}
一共有三种方式可以获取一个类在运行时的Class对象,分别是:
- 使用Class类的forName()静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。比如:Class.forName("com.Student")
- 调用某个类的class属性来获取该类对应的Class对象。例如Person.class将会返回Person类对应的Class对象。
- 调用某个对象的getClass()方法,该方法是java.lang.Object类中的一个方法,所以所有Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。比如:student.getClass()
实例代码:
package FleshTest;
/**
* @author: 随风飘的云
* @describe:类对象的获取
* @date 2022/08/22 16:21
*/
public class Student {
public static void getClassTest(){
Class<?> invocation1 = null;
Class<?> invocation2 = null;
Class<?> invocation3 = null;
// 第一种
try {
// 最常用的方法
invocation1 = Class.forName("FleshTest.Student");
}catch (Exception ex){
ex.printStackTrace();
}
// 第三种
invocation2 = new Student().getClass();
// 第二种
invocation3 = Student.class;
System.out.println(invocation1);
System.out.println(invocation2);
System.out.println(invocation3);
}
public static void main(String[] args) {
getClassTest();
}
}
结果:
再来看看 Class类的方法:
获取指定类型的实例化对象
构建Student类对象信息,后面的不再重复补充。
package FleshTest;
public class Student {
private int age;
private String name;
public Student() {
}
public Student(int age) {
this.age = age;
}
public Student(String name) {
this.name = name;
}
public Student(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
实例代码:
package FleshTest;
/**
* @author: 随风飘的云
* @describe:获取指定类型的实例化对象
* @date 2022/08/22 16:28
*/
public class MyInvocation2 {
public static void getInstanceTest() {
try {
Class<?> studentClass = Class.forName("FleshTest.Student");
Student student = (Student) studentClass.newInstance();
student.setAge(22);
student.setName("随风飘的云");
System.out.println(student);
}catch (Exception ex){
ex.printStackTrace();
}
}
public static void main(String[] args) {
getInstanceTest(); // 输出:Student{age=22, name='随风飘的云'}
}
}
如果说删掉上面Student类中的无参构造方法,那会发生什么呢?结果:
分析: 因为代码中重写了构造方法,而且是有参构造方法,如果不写构造方法,那么每个类都会默认有无参构造方法,重写了就不会有无参构造方法了,所以我们调用newInstance()的时候,会报没有这个方法的错误。值得注意的是,newInstance()是一个无参构造方法。
构造函数对象实例化对象
除了newInstance()方法之外,还可以通过构造函数对象获取实例化对象,怎么理解?这里只构造函数对象,而不是构造函数,也就是构造函数其实就是一个对象,我们先获取构造函数对象,当然也可以使用来实例化对象。 获取构造方法:
package FleshTest;
import java.lang.reflect.Constructor;
public class MyInvocation3 {
public static void testConstruct(){
try {
Class<?> student = Class.forName("FleshTest.Student");
Constructor<?> cons[] = student.getConstructors();
for(int i=0;i<cons.length;i++){
System.out.println(cons[i]);
}
}catch (Exception ex){
ex.printStackTrace();
}
}
public static void main(String[] args) {
testConstruct();
}
}
结果:
获取到了一个类的构造函数,那么就可以通过构造函数获取到它的各种信息,包括参数,参数个数,类型等等:
public static void testConstruct(){
try {
Class<?> student = Class.forName("FleshTest.Student");
Constructor<?> cons[] = student.getConstructors();
Constructor constructors = cons[0];
System.out.println("name: " + constructors.getName());
System.out.println("modifier: " + constructors.getModifiers());
System.out.println("parameterCount: " + constructors.getParameterCount());
System.out.println("构造参数类型如下:");
for (int i = 0; i < constructors.getParameterTypes().length; i++) {
System.out.println(constructors.getParameterTypes()[i].getName());
}
}catch (Exception ex){
ex.printStackTrace();
}
}
结果: modifier是权限修饰符,1表示为public,我们可以知道获取到的构造函数是两个参数的,第一个是int,第二个是String类型,看来获取出来的顺序应该是书写代码的顺序。
既然已经获取到了一个类的构造方法了,那么就可以通过这个方法进行构造对象:
public static void constructGetInstanceTest() {
try {
Class<?> stduent = Class.forName("FleshTest.Student");
Constructor<?> cons[] = stduent.getConstructors();
// 一共定义了4个构造器
Student student1 = (Student) cons[0].newInstance(22, "随风飘的云");
Student student2 = (Student) cons[1].newInstance("随风飘的云");
Student student3 = (Student) cons[2].newInstance(22);
Student student4 = (Student) cons[3].newInstance();
System.out.println(student1);
System.out.println(student2);
System.out.println(student3);
System.out.println(student4);
} catch (Exception ex) {
ex.printStackTrace();
}
}
结果:
需要注意的是:构造器的顺序我们是必须一一针对的,要不会报一下的参数不匹配的错误:
// 一共定义了4个构造器
Student student1 = (Student) cons[3].newInstance(22, "随风飘的云");
Student student2 = (Student) cons[2].newInstance("随风飘的云");
Student student3 = (Student) cons[1].newInstance(22);
Student student4 = (Student) cons[0].newInstance();
结果:
获取类继承的接口
通过反射我们可以获取接口的方法,如果我们知道某个类实现了接口的方法,同样可以做到通过类名创建对象调用到接口的方法。 实例代码:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
interface Animal {
public void Is_Alive();
}
interface PetClass {
public void PetName();
}
public class Cat implements Animal, PetClass{
@Override
public void Is_Alive() {
System.out.println("猫是活的");
}
@Override
public void PetName() {
System.out.println("猫可以作为宠物培养");
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("FleshTest.Cat");
Class<?>[] interfaces = clazz.getInterfaces();
for (Class c: interfaces) {
// 获取接口
System.out.println(c);
// 获取接口的方法
// 获取接口
System.out.println(c);
// 获取接口里面的方法
Method[] methods = c.getMethods();
// 遍历接口的方法
for (Method method : methods) {
// 通过反射创建对象
Cat cat = (Cat) clazz.newInstance();
// 通过反射调用方法
method.invoke(cat, null);
}
}
}
}
结果: 这样子可以获取到接口的数组,并且里面的顺序是我们继承的顺序,通过接口的Class对象,我们可以获取到接口的方法,然后通过方法反射调用实现类的方法,因为这是一个无参数的方法,所以只需要传null即可。
获取父类相关信息
如何获取父类的相关信息,可以使用getSuperclass()方法获取父类,当然也可以获取父类的方法,执行父类的方法。 Animal.java:
package FleshTest.Test;
public class Animal {
public void doSomething() {
System.out.println("animal do something");
}
}
Dog.java继承于Animal.java:
package FleshTest.Test;
public class Dog extends Animal{
public void doSomething(){
System.out.println("Dog do something");
}
}
测试代码:
package FleshTest.Test;
import java.lang.reflect.Method;
public class TestClass {
public static void main(String[] args) throws Exception {
Class<?> dogClass = Class.forName("FleshTest.Test.Dog");
System.out.println(dogClass);
invoke(dogClass);
Class<?> animalClass = dogClass.getSuperclass();
System.out.println(animalClass);
invoke(animalClass);
Class<?> objectClass = animalClass.getSuperclass();
System.out.println(objectClass);
invoke(objectClass);
}
public static void invoke(Class<?> myClass) throws Exception {
Method[] methods = myClass.getMethods();
// 遍历接口的方法
for (Method method : methods) {
if (method.getName().equalsIgnoreCase("doSomething")) {
// 通过反射调用方法
method.invoke(myClass.newInstance(), null);
}
}
}
}
结果:
获取当前类的公有属性和私有属性以及更新
使用getFields()可以获取到public的属性,包括static属性,使用getDeclaredFields()可以获取所有声明的属性,不管是public,protected,private不同修饰的属性。 修改public属性,只需要field.set(object,value)即可,但是private属性不能直接set,private默认是不允许外界操作其值的,这里我们可以使用field.setAccessible(true);相当于打开了操作的权限。 static的属性修改和非static的一样,但是我们怎么获取呢?如果是public修饰的,可以直接用类名获取到,如果是private修饰的,那么需要使用filed.get(object),这个方法其实对上面说的所有的属性都可以的。 实例代码:
package FleshTest;
import java.lang.reflect.Field;
class Person {
public static String type ;
private static String subType ;
// 名字(公有)
public String name;
protected String gender;
private String address;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
public class TestPerson {
public static void main(String[] args) throws Exception{
Class<?> personClass = Class.forName("FleshTest.Person");
Field[] fields = personClass.getFields();
// 获取公开的属性
for(Field field:fields){
System.out.println(field);
}
System.out.println("=================");
// 获取所有声明的属性
Field[] declaredFields = personClass.getDeclaredFields();
for(Field field:declaredFields){
System.out.println(field);
}
System.out.println("=================");
Person person = (Person) personClass.newInstance();
person.name = "Sam";
System.out.println(person);
// 修改public属性
Field fieldName = personClass.getDeclaredField("name");
fieldName.set(person,"Jone");
// 修改private属性
Field addressName = personClass.getDeclaredField("address");
// 需要修改权限
addressName.setAccessible(true);
addressName.set(person,"东风路47号");
System.out.println(person);
// 修改static 静态public属性
Field typeName = personClass.getDeclaredField("type");
typeName.set(person,"人类");
System.out.println(Person.type);
// 修改静态 private属性
Field subType = personClass.getDeclaredField("subType");
subType.setAccessible(true);
subType.set(person,"黄种人");
System.out.println(subType.get(person));
}
}
结果: 从结果可以看出,不管是public,还是protected,private修饰的,我们都可以通过反射对其进行查询和修改,不管是静态变量还是非静态变量。getDeclaredField()可以获取到所有声明的属性,而getFields()则只能获取到public的属性。对于非public的属性,我们需要修改其权限才能访问和修改:field.setAccessible(true)
。获取属性值需要使用field.get(object),值得注意的是:每个属性,其本身就是对象
获取以及调用类的公有/私有方法
既然上文中提到可以获取到公有和私有的方法,那是否可以通过反射执行公有或者是私有的方法呢? 创建People类:
package FleshTest;
class People{
// 非静态公有无参数
public void read(){
System.out.println("reading...");
}
// 非静态公有无参数有返回
public String getName(){
return "Sam";
}
// 非静态公有带参数
public int readABookPercent(String name){
System.out.println("read "+name);
return 80;
}
// 私有有返回值
private String getAddress(){
return "东方路";
}
// 公有静态无参数无返回值
public static void staticMethod(){
System.out.println("static public method");
}
// 公有静态有参数
public static void staticMethodWithArgs(String args){
System.out.println("static public method:"+args);
}
// 私有静态方法
private static void staticPrivateMethod(){
System.out.println("static private method");
}
}
获取People类里面的方法:
public static void GetAllMethods() throws Exception {
Class<?> personClass = Class.forName("FleshTest.Person");
Method[] methods = personClass.getMethods();
for (Method method : methods) {
System.out.println(method);
}
System.out.println("=============================================");
Method[] declaredMethods = personClass.getDeclaredMethods();
for (Method method : declaredMethods) {
System.out.println(method);
}
}
结果: 我们发现getMethods()确实可以获取所有的公有的方法,但是有一个问题,就是他会把父类的也获取到,也就是下面图片后色框里面的,我们知道所有的类默认都继承了Object类,所以它把Object的那些方法都获取到了。而getDeclaredMethods确实可以获取到公有和私有的方法,不管是静态还是非静态,但是它是获取不到父类的方法的。
首先试一下调用非静态方法:
public static void getNoStatic() throws Exception {
Class<?> personClass = Class.forName("FleshTest.People");
People people = (People) personClass.newInstance();
Method[] declaredMethods = personClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if(method.getName().equalsIgnoreCase("read")){
method.invoke(people,null);
System.out.println("===================");
}else if(method.getName().equalsIgnoreCase("getName")){
System.out.println(method.invoke(people,null));
System.out.println("===================");
}else if(method.getName().equalsIgnoreCase("readABookPercent")){
System.out.println(method.invoke(people,"随风飘的云"));
System.out.println("===================");
}
}
}
结果:
那如果调用私有方法呢?
public static void getNoPublic() throws Exception {
Class<?> personClass = Class.forName("FleshTest.People");
People people = (People) personClass.newInstance();
Method[] declaredMethods = personClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if(method.getName().equalsIgnoreCase("getAddress")){
System.out.println(method.invoke(people, null));
}
}
}
结果: 这是因为不具有访问权限导致的。
修改代码添加允许访问权限的代码语句:
public static void getNoPublic() throws Exception {
Class<?> personClass = Class.forName("FleshTest.People");
People people = (People) personClass.newInstance();
Method[] declaredMethods = personClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if(method.getName().equalsIgnoreCase("getAddress")){
// 添加这行语句
method.setAccessible(true);
System.out.println(method.invoke(people, null));
}
}
}
结果:
最后,如果想要调用静态方法或者是静态私有方法,那该怎么办呢?
public static void getStaticMethod() throws Exception {
Class<?> peopleClass = Class.forName("FleshTest.People");
People people = (People) peopleClass.newInstance();
Method[] declaredMethods = peopleClass.getDeclaredMethods();
for (Method method : declaredMethods) {
if(method.getName().equalsIgnoreCase("staticMethod")){
System.out.println("===================");
}else if(method.getName().equalsIgnoreCase("staticMethodWithArgs")){
System.out.println(method.invoke(people,"随风飘的云"));
System.out.println("===================");
}else if(method.getName().equalsIgnoreCase("staticPrivateMethod")){
// 设置方法允许访问
method.setAccessible(true);
System.out.println(method.invoke(people,null));
System.out.println("===================");
}
}
}
结果:
那如果不想使用遍历的方法调用这些方法,那该怎么办呢?
public static void getNoFor() throws Exception {
Class<?> peopleClass = Class.forName("FleshTest.People");
People people = (People) peopleClass.newInstance();
Method method1 = peopleClass.getMethod("readABookPercent", String.class);
method1.invoke(people, "唐诗三百首");
Method method2 = peopleClass.getMethod("getName", null);
System.out.println(method2.invoke(people, null));
}
结果: