查漏补缺,超详细的Java基础知识点总结,快看哪些你还不会?
它是基础的线性储存结构,也可以有效的实现一组变量的关联
1.“数组名称.length”属性,可以直接得到数组的长度2.数组属于引用数据类型,在代码中需要进行内存的分配3.动态初始化后会发现数组中的每一个元素的内容都是其对应数据类型的默认值4.静态初始化:在数组定义时就为其设置了具体的数据内容,这样就避免了先开辟后赋值的重复操作
在数组使用时需要关键字new开辟堆内存空 间,而一块堆内存空间可以被多个栈内存指向,自然也就能对它进行引用数据操作注意:不能够直接使用未开辟堆内存空间的数组(因为一但使用就会出现空指向异常)
程序结果:
他是一种加强型for循环,使用foreach结构可以实现数组数据的输出
如下:for(数据类型 变量 :数组 | 集合){//循环体代码,循环的次数为数组的长度}
程序的执行结果:
它是一种描述多行多列的数组结构
它也有俩种初始化的方式,分别是动态初始化和静态初始化(其实要区分动态和静态,就看初始化时有没有进行赋值操作,有的话就是静态的)1.动态初始化:数据类型 数组名称[][] = new 数据类型[行的个数][列的个数];2.静态初始化:数据类型 数组名称[][] = new 数据类型[][]{{值,值,值}{值,值,值}}关于多维数组::二维及其以上的都可以称为多维数组,但维数越高,复杂度也就越高,在开发的过程中建议尽量不要使用多维数组
程序执行结果:
这里要讲的是如何利用方法接收数组和返回数组的操作
1.用方法接收一个数组:事先开辟好一个数组,然后通过引用传递,指向该数组的堆空间,这样就达到了接收的目的了2.用方法返回一个数组:和接收差不多,只不过接收时对方法的类型没有强制的要求,返回时则必须要要求方法的类型与接收数组的类型一致注:因为用的是引用传递只要有一个修改了堆空间里内容,其它的也会跟着改变,因为归根结底指向的都是同一块堆空间。
程序执行结果:
除了基本数据类型的数据定义为数组外,引用数据类型也可以定义数组,像这样的就是对象数组了
1.对象数组动态初始化:类 对象名称[] = new 类 [长度];2.对象数组静态初始化:类 对象名称[] = new 类 [实例化对象,实例化对象,…];类的定义和以前的差不多,只不过在实例化对象的时候用了数组的形式
程序执行结果:
这里要说的是一个类可以关联其他类。根据引用关系依据某一个类对象,可以获取其相应的信息
详解:用于描述连个类之间的引用关系,在主类操作时首先根据两个类的关系设置引用关系,随后就可以根据引用关系依据某一个类对象获取相应的关系(这里有一点象父类与子类的继承的意思在里面,但两者的操作是不一样的)
可以利用自身关联的形式描述人员后代的关系
详解:在进行类关联描述的过程中,除了可以关联其他类之外,也可以实现自身的关联操作,也就是说可以利用自身关联的形式描述人员后代的关系
将对象的引用关联进一步扩展就可以实现更多的结构描述,通过不同的类实现子结构定义,随后将其在一个父结构中整合
详解:下面会写一个伪代码的合成设计模式组成结构。需要注意的是Java中提供的引用数据类型不仅仅是描述的内存操作形式,更多的是包含了抽象与关联的设计思想
这里分析的是数据表与简单java类之间的转化。简当操作就是首先实例化各个对象信息,随后根据关联关系设置数据间的引用配置,在数据配置完成后就可以依据对象间的引用关系获取对象的相应信息。
程序执行结果:
详解:现代项目的开发中数据库是核心的组成部分,几乎所有的项目代码都是围绕着数据表的业务逻辑结构展开的,那么分析数据表与简单java类之间的转化也是很重要的
1.数组是一组相关数据变量的线性集合,利用数组可以方便地实现一组变量的关联,但数组的缺点就在于长度不可以改变
2.数组的访问需要通过“数组名称【索引】”的形式,如果超出索引范围就会报异常。
3.数组的长度可以用“数组名称.length”的形式动态获取。
4. 数组采用动态初始化时,数组中的每一个元素的内容都是其对应数据类型的默认值。
5.如果使用了未开辟堆内存空间的数组则会出现空指向异常。
6.在JDK中使用System.arraycopy()和java.util.Arrays.sort两个方法可以实现数组的复制和排序
7.JDK1.5之后追加了可变参数,这样可以使得方法可以任意接收多个参数。不过需注意的是接的参数使用数组形式处理。
8.对象数组可以实现一组对象的管理,这样在开发中就可以描述多个实例。
9.简单java类可以实现数据表结构的映射转换,通过面向对象的关联形式可以描述数据表的存储结构
原文链接:https://blog.csdn.net/weixin_52533616/article/details/113827437
以上就是小编分享给大家的Java基础知识点总结,小编这里还总结了一份Java核心知识点学习笔记一份,下面是部分资料截图,编程的世界永远向所有热爱编程的人开放,这是一个自由,平等,共享的世界,我始终是这样坚信的。
该资料获取方式:转发后,关注我私信【学习笔记】获取Java核心知识点学习笔记
最后
整理不易,欢迎大家一起交流,喜欢文章记得关注我点赞哟,感谢支持!重要的事情说三遍,转发+转发+转发,一定要记得转发 关注 领取哦!!!
Java基础知识总结(超详细整理)
Java语言的特点
1.面向对象
面向对象(OOP)就是Java语言的基础,也是Java语言的重要特性。面向对象的概念:生活中的一切事物都可以被称之为对象,生活中随处可见的事物就是一个对象,我们可以将这些事物的状态特征(属性)以及行为特征(方法)提取并出来,并以固定的形式表示。
2.简单好用
Java语言是由C和C++演变而来的,它省略了C语言中所有的难以理解、容易混淆的特性(比如指针),变得更加严谨、简洁、易使用。
3.健壮性
Java的安全检查机制,将许多程序中的错误扼杀在摇蓝之中。 另外,在Java语言中还具备了许多保证程序稳定、健壮的特性(强类型机制、异常处理、垃圾的自动收集等),有效地减少了错误,使得Java应用程序更加健壮。
4.安全性
Java通常被用在网络环境中,为此,Java提供了一个安全机制以防恶意代码的攻击,从而可以提高系统的安全性。
5.平台无关性
Java平台无关性由Java 虚拟机实现,Java软件可以不受计算机硬件和操作系统的约束而在任意计算机环境下正常运行。
6.支持多线程
在C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持。多线程机制使应用程序在同一时间并行执行多项任务,该机制使得程序能够具有更好的交互性、实时性。
7.分布式(支持网络编程)
Java语言具有强大的、易于使用的网络能力,非常适合开发分布式计算的程序。java中提供了网络应用编程接口(java.net),使得我们可以通过URL、Socket等远程访问对象。
8.编译与解释共存
Java 是编译与解释共存的语言
Java语法基础
标识符: 用来标识类名、对象名、变量名、方法名、类型名、数组名、文件名的有效字符序列。
合法的标识符:
由字母、数字、下划线“_”、美元符号“$”或者“¥”组成,并且首字符不能是数字。
不能把java关键字和保留字作为标识符。
标识符对大小写敏感。
关键字:Java语言中已经赋予了特定含义的
保留字: const、goto,Java版本中尚未使用,但以后版本可能会作为关键字使用
变量:程序运行期间可以被改变的量。在程序中使用变量,必须先创建它并为它取一个名字,并且指明它能够存储信息的类型,这称为“变量声明”,也叫容器的创建。
变量的使用:
变量的声明:数据类型 变量名;
变量的赋值:变量名 = 数据;
变量的操作:放入打印语句进行输出或者进行运算
Java 中的注释有三种:
单行注释
多行注释
文档注释
数据类型
基本数据类型(8个),也称原始数据类型:
引用数据类型(3种):数组、类、接口
类型转换:
自动类型转换,也叫隐式转换
强制类型转换,也叫显式转换
运算符
运算符:一种特殊符号,用以表示数据的运算、赋值和比较数与整数
运算符分类:算术运算符、赋值运算符、比较运算符、逻辑运算符、三元运算符
1.算术运算符
2.赋值运算符
3.比较运算符
4.逻辑运算符
5.三元运算符
条件表达式b?x:y;,先计算条件b,然后进行判断。如果b的值为true,计算x的值,运算结果为x的值;否则,计算y的值,运算结果为y的值。
Java流程控制语句
选择结构
if语句: if(条件表达式){ 一条或多条语句 };
if else语句: if(条件表达式) {语句块1} else {语句块2}
if多分支语句:
switch开关语句:
循环结构
在程序中当要重复性的做某项工作时可以使用循环语句,包括:for循环、while循环、do…while循环。
for循环语句:
while循环语句:
do…while循环语句:
流程跳转
流程跳转语句:break,continue
break:在switch中结束case条件判断,在循环体中结束循环
continue:作用在循环体中,结束循环体的本次循环,而进入下次循环
数组
数组是数据的集合,一个容器,用来存储任何类型的数据,包括原始数据类型和引用数据类型,但是一旦指定了数组的类型之后,就只能用来存储指定类型的数据。
数组声明的三种方式:
数据类型[] 数组名 = new 数据类型[长度];
数据类型[] 数组名 = {数据,数据,…,数据};
数据类型[] 数组名 = new 数据类型长度[] {数据,数据,…,数据};
一维数组:
数组变量的声明:
语法:数据类型[] 数组名;,如:int[] num;、double[] d;、String[] str;
数组对象的创建:
语法:数组名 = new 数据类型[长度];,如:num = new int[4];,数组声明其长度后不可改变
赋值:
语法:数组名[下标] = 数据;,如:num[0] = 3;
数组元素的使用及遍历:
语法:数组名[下标],获取指定下标是数据。
二维数组:
数组变量的声明:
语法:数据类型[][] 数组名;,如:int[][] num;、double[][] d;、String[][] str;
数组对象的创建:
语法:数组名 = new 数据类型[外长度][内长度];,如:num = new int[4][3];,数组声明其长度后不可改变
赋值:
语法:数组名[外下标][内下标] = 数据;,如:num[0][0]= 3;
数组元素的使用及遍历:
语法:数组名[外下标][内下标],获取指定下标是数据。
对象与类
对象:客观存在能够相互区分的个体,比如这辆汽车、这个人、这间房子、这张桌子、这株植物、这张支票、这件雨衣。概括来说就是:万物皆对象。
类:若干具有相同属性和行为的对象的群体或者抽象,类是创建对象的模板,由属性和行为两部分组成
类是对象的概括或者抽象,对象是类的实例化。
类的声明
成员变量的声明: [修饰符] 数据类型 变量名 [= 初始值];
成员方法的声明:
构造器的声明:
注意:
①构造器名必须和类名一致
②构造器没有返回值类型
③任何类都含有构造器。如果没有显式地定义类的构造器,
④则系统会为该类提供一个默认的无参的构造器。一旦在类中显式地定义了构造器,系统就不会再为这个类提供默认的构造器了。
类的使用
类变量的声明:类名 对象名;
类对象的创建,赋值给变量:对象名 = new 构造器([参数列表]);
对象属性的赋值:属性:对象名.属性名、对象名.属性名 = 数据;
属性和方法的调用:属性:System.out.println(对象名.属性名);方法:对象名.方法名();
成员变量与局部变量:
成员变量:声明在类中方法体之外、可以有缺省值、可以使用修饰符。作用域:整个类
局部变量:声明在方法体或代码块中、没有缺省值、只能使用final修饰。作用域:当前方法体
参数:参数的本质是特殊的局部变量,只能定义在方法的小括号中
重载
方法的重载OverLoading: 同一个类中定义了多个方法名相同而参数不同的方法
重载在同一个类中,方法名相同,参数不同(参数的个数、顺序、类型不同)
构造器的重载:
方法的返回值类型:
无返回值类型:void,return;//结束方法体
有返回值类型为:数据类型(基本数据类型、引用数据类型),return 数据;//结束方法体,并且返回一条数据
关键字
this关键字:
this是一种特殊的引用,指向当前对象
this的两种使用方法:
如果发生局部变量和成员变量命名冲突时,可以通过this.成员变量名的方式区分成员变量和局部变量。
一个构造方法中需要调用本类的另一个构造方法,可以通过this()的方式调用,但this()必须要书写在第一行。
static关键字:
静态变量: 使用static修饰的成员变量叫做静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。
静态方法: 使用static修饰的成员方法叫做静态方法,静态方法可以不依赖于任何对象进行访问(对于静态方法来说,是没有this的),由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。
调用格式:类名.静态变量名 ,类名.静态方法名()
静态内部类(static 修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非 static 成员变量和方法。
静态代码块:
是一个以static为前导的代码块,一般用于为类的工作做一些初始化工作,如初始化一些静态变量。一个类中可以有许多静态初始化块,并且它们可以出现在类体的任何地方。运行时系统会保证静态初始化块会按照它们在源代码中出现的顺序被调用
static块可以用来优化程序性能:因为它只会在类加载的时候执行一次
super关键字:
super代表的是父类对象,
使用方式:
super.属性名、super.方法名();用于在子类中调用父类被隐藏的同名实例变量
super([参数列表])用于在子类的构造方法中调用父类的构造方法
每一个子类的构造方法在没有显示调用super()系统都会提供一个默认的super(),super()必须是构造器的第一条语句
final关键字:
final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:
修饰类:类不能继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
修饰符变量:该变量为常量,,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
修饰符方法:方法不能重写
说明:使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。
访问修饰符
访问修饰符对成员变量和成员方法的访问限定
Java的三大特性
封装
封装指隐藏对象的状态信息(属性),不允许外部对象直接访问对象的内部信息(private实现)。但是可以提供一些可以被外界访问的方法来操作属性。
将类中成员变量private,提供public的get和set方法来控制属性的存取动作,以保证对私有属性操作的安全性:
继承
继承extends:多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承单独的那个类即可。多个类可以称为子类,单独这个类称为父类或者超类。
基本语法:
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的属性或方法(对父类进行扩展),也可以拥有父类的属性和方法,并且通过自己的方法再次实现父类的方法(重写)。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
Java只支持单继承,不支持多继承。一个类只能有一个父类,不可以有多个父类。Java支持多层继承(继承体系)。Java继承了父类非私有的成员变量和成员方法,但是请注意:子类是无法继承父类的构造方法的。
注意:不要仅为了获取其他类中某个功能而去继承 ,类与类之间要有所属(“is a” )关系
方法的重写:
子类从父类继承的某个实例方法无法满足子类的功能需要时,需要在子类中对该实例方法进行重新实现,这样的过程称为重写,也叫做覆写、覆盖。
方法重写的前提:继承,子类的修饰符大于等于父类,方法名、参数列表、返回值类型必须相同
多态
多态:一种事物的多种形态(多态的前提:继承、重写、向上转型)
通过多态可以提高代码的可重用性,降低模块之间的耦合度。
抽象类
抽象类的概念:
Java中可以定义没有方法体的方法,该方法的具体实现由子类完成,该方法称为抽象方法,包含抽象方法的类就是抽象类。如:Shape类计算周长和面积的方法无法确定,那么就可以将这样的方法声明为抽象的,以便在具体的子类中进行实现。
抽象类的声明: [修饰符] abstract class 类名 [extends 父类名]{类体}
抽象方法的声明: [修饰符] abstract 返回值类型 方法名([参数列表]);
因为抽象方法无法确定具体执行的功能,所有抽象方法没有方法体,需要在小括号后加上分号
抽象类和普通类除了使用abstract修饰外和普通的类相似,抽象类中可以没有抽象方法,但是一旦某个有抽象方法,那么这个类必须被声明为抽象类。
抽象类的使用:
因为抽象类不是一个具体的类,所以无法实例化,但是抽象类可以用于声明变量
抽象类可以被继承,在子类中实现抽象类的所有抽象方法,以达到抽象类的具体化
接口
在Java中接口不仅仅是程序开发过程中“约定”,更是更加抽象化的抽象类。
接口的声明语法: [修饰符] interface 接口名{[常量];[抽象方法];}
接口实现的语法: [修饰符] class 类名 [extends 父类名] [implements 接口1,接口2,……]{类体部分}
一个类可以实现多个接口,从而解决了Java单继承的缺点。
接口的作用:
提高程序的重用性
提高程序的可扩展性
降低程序的耦合度
实现了多继承
小伙伴们有兴趣想了解内容和更多相关学习资料的请点赞收藏+评论转发+关注我,后面会有很多干货。我有一些面试题、架构、设计类资料可以说是程序员面试必备!所有资料都整理到网盘了,需要的话欢迎下载!私信我回复【111】即可免费获取
版权声明:本文为CSDN博主「龙源lll」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Lzy410992/article/details/118852573
Java 基础常见知识点&面试题总结,2022 最新版
原文地址:https://mp.weixin.qq.com/s/PQA_sB5J2nK05ilKUDz0mQ侵权联系删除
两者的主要区别在于解决问题的方式不同:
- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
另外,面向对象开发的程序一般更易维护、易复用、易扩展。
相关 issue : 面向过程 :面向过程性能比面向对象高??
- 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
- 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
- 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
new 运算符,new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。
一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。
如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了,我们一直在不知不觉地使用构造方法,这也是为什么我们在创建对象的时候后面要加一个括号(因为要调用无参的构造方法)。如果我们重载了有参的构造方法,记得都要把无参的构造方法也写出来(无论是否用到),因为这可以帮助我们在创建对象的时候少踩坑。
构造方法特点如下:
- 名字与类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类的对象时自动执行,无需调用。
构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。
不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(以后介绍)。
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
多态的特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
共同点 :
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。
区别 :
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类,)。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
关于深拷贝和浅拷贝区别,我这里先给结论:
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
上面的结论没有完全理解的话也没关系,我们来看一个具体的案例!
浅拷贝
浅拷贝的示例代码如下,我们这里实现了 Cloneable 接口,并重写了 clone() 方法。
clone() 方法的实现很简单,直接调用的是父类 Object 的 clone() 方法。
测试 :
从输出结构就可以看出, person1 的克隆对象和 person1 使用的仍然是同一个 Address 对象。
深拷贝
这里我们简单对 Person 类的 clone() 方法进行修改,连带着要把 Person 对象内部的 Address 对象一起复制。
测试 :
从输出结构就可以看出,虽然 person1 的克隆对象和 person1 包含的 Address 对象已经是不同的了。
那什么是引用拷贝呢? 简单来说,引用拷贝就是两个不同的引用指向同一个对象。
我专门画了一张图来描述浅拷贝、深拷贝、引用拷贝:
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
== 对于基本类型和引用类型的作用效果是不同的:
- 对于基本数据类型来说,== 比较的是值。
- 对于引用数据类型来说,== 比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
Object 类 equals() 方法:
equals() 方法存在两种使用情况:
- 类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
- 类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 == 换成 equals() ):
String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
String类equals()方法:
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是:Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
其实, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!
我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说hashCode 帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode 值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是 :
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode() 和 equals() 的介绍之后,下面这个问题已经难不倒你们了。
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
思考 :重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。
总结 :
- equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
- 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。
更多关于 hashCode() 和 equals() 的内容可以查看:Java hashCode() 和 equals()的若干问题解答
可变性
String 是不可变的(后面会详细分析原因)。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用 String
- 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
String 类中使用 final 关键字修饰字符数组来保存字符串,所以String 对象是不可变的。
修正 :我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。
String 真正不可变有下面几点原因:
保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
相关阅读:如何理解 String 类型值的不可变?- 知乎提问
补充(来自issue 675):在 Java 9 之后,String 、StringBuilder 与 StringBuffer 的实现改用 byte 数组存储字符串。
public final class String implements java.io.Serializable,Comparable<String>, CharSequence {// @Stable 注解表示变量最多被修改一次,称为“稳定的”。@Stableprivate final byte[] value;}abstract class AbstractStringBuilder implements Appendable, CharSequence {byte[] value;}
Java 9 为何要将 String 的底层实现由 char[] 改成了 byte[] ?
新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。
如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的。
这是官方的介绍:https://openjdk.java.net/jeps/254 。
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符。
上面的代码对应的字节码如下:
可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。
如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。
如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。
String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。Object 的 equals 方法是比较的对象的内存地址。
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
更多关于字符串常量池的介绍可以看一下 Java 内存区域详解 这篇文章。
会创建 1 或 2 个字符串对象。
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象“abc”。
示例代码(JDK 1.8):
对应的字节码:
ldc 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
示例代码(JDK 1.8):
对应的字节码:
这里就不对上面的字节码进行详细注释了,7 这个位置的 ldc 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 ldc 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 ldc 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。
String.intern() 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
示例代码(JDK 1.8) :
先来看字符串不加 final 关键字拼接的情况(JDK1.8):
注意 :比较 String 字符串的值是否相等,可以使用 equals() 方法。String 中的 equals 方法是被重写过的。Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是字符串的值是否相等。如果你使用 == 比较两个字符串是否相等的话,IDEA 还是提示你使用 equals() 方法替换。
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到:
常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。
对于 String str3 = \”str\” + \”ing\”; 编译器会给你优化成 String str3 = \”string\”;。
并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:
- 基本数据类型( byte、boolean、short、char、int、float、long、double)以及字符串常量。
- final 修饰的基本数据类型和字符串变量
- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )
引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 StringBuilder 或者 StringBuffer。
不过,字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。
示例代码:
被 final 关键字修改之后的 String 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。
如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。
示例代码(str2 在运行时才能确定其值):
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。