java
注:本笔记是对以前学习内容的总结,因此,很容易出现一些多线程,JVM,框架,@Component注解等。如果你无法理解,可以前往元动力官网,这个网站的两位老师很厉害,本文就是基于他们的笔记来写的。
序章 计算机基础知识
二进制的存储
计算机中,所有数据都是以二进制来进行存储的,在这些之中
- 1bit 一位
- 1Byte = 8bit 一字节
- 1KB = 1024B
- 1MB = 1024KB
- 1GB = 1024MB
- 1TB = 1024GB
网络
网络就是通过网线,光缆,路由器,交换机等设备将一个计算机的信号传送到另一个计算机上。咱们的课程不是计算机网络,到了后期会详细的介绍网络中的一些协议。
传输的就是信号,可以是电信号,也可以是光信号。电信号高电平代表一,低电平代表0。
网络带宽是啥意思,就是1s能传输多少个0或1。
比如说你家的网络是 200M,其实真实的单位是200MHZ,上个频率的单位。
运营商的单位是 Mbit
我们的下载速度是 MByte
下载理论速度是 200/8 = 25M/s
1. java
1.1 Java是一种计算机编程语言
- 编程是把我们的要求和想法,按照能够让计算机看懂的规则和约定编写出来。编程的结果就是一些计算机能够看懂并能够执行和处理的东西,我们把它叫做软件或者程序。事实上,程序就是我们对计算机发出的命令集(指令集)。
- 语言是用来交流的,和你说的汉语很向,但是汉语是人与人交流,java是一种人与计算机交流的语言。换句话说把我们的要求和想法用Java 表达出来,那么计算机能看懂,就能够按照我们要求运行,而这个过程就是我们说的使用Java 编程,所以我们讲Java 是一种计算机编程语言。
- 为了让计算机看懂,Java 会有一系列的规则和约定,这些就是Java 的语法。
1.2 Java能干什么
我们可以用java语言开发出应用程序,部署并运行在安装有java环境的计算机上。
(1)桌面级应用
先解释一下桌面级应用:简单的说就是主要功能都在我们本机上运行的程序,比如word 、excel 等运行在本机上的应用就属于桌面应用,尤其是需要跨平台的桌面级应用程序,但是目前使用java做桌面级开发的场景确实越来越少。
(2)企业级应用
先解释一下企业级应用:简单的说就是大规模的应用,一般使用人数较多,数据量较大,对系统的稳定性、安全性、可扩展性和可装配性等都有比较高的要求。
这是目前Java 应用最广泛的一个领域,几乎一枝独秀。包括各种行业应用,企业信息化,也包括电子政务等,领域涉及:办公自动化OA,客户关系管理CRM,人力资源HR,企业资源计划ERP 、知识管理KM、供应链管理SCM 、企业设备管理系统EAM 、产品生命周期管理PLM 、面向服务体系架构SOA 、商业智能BI、项目管理PM、营销管理、流程管理WorkFlow 、财务管理……等等几乎所有你能想到的应用。
其实这个概念已经慢慢淡化了,我们现在进入了互联网时代,一个软件的体量已经不能同日而语,现在的软件需要支持更高的并发,追求更稳定的效果。
(3)大数据领域产品
很多大数据领域使用的软件都是java编写的,所有java学习也是大数据学习的基础。
1.3 Java语言的特点
Java 语言是一种纯粹的面向对象语言,它继承了 C++ 语言面向对象的技术核心,但是拋弃了 C++ 的一些缺点,比如说容易引起错误的指针以及多继承等,同时也增加了垃圾回收机制,释放掉不被使用的内存空间,解决了管理内存空间的烦恼。
Java 语言是一种分布式的面向对象语言,具有面向对象、平台无关性、简单性、解释执行、多线程、安全性等很多特点,下面针对这些特点进行逐一介绍。
(1)面向对象
Java 是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包等均有很好的支持。为了简单起见,Java 只支持类之间的单继承,但是可以使用接口来实现多继承。使用 Java 语言开发程序,需要采用面向对象的思想设计程序和编写代码。
(2) 平台无关性
平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
Java 语言使用 Java 虚拟机机制屏蔽了具体平台的相关信息,使得 Java 语言编译的程序只需生成虚拟机上的目标代码,就可以在多种平台上不加修改地运行。
(3) 简单性
Java 语言的语法与 C 语言和 C++ 语言很相近,使得很多程序员学起来很容易。对 Java 来说,它舍弃了很多 C++ 中难以理解的特性,如操作符的重载和多继承等,而且 Java 语言不使用指针,加入了垃圾回收机制,解决了程序员需要管理内存的问题,使编程变得更加简单。
(4)解释执行
Java 程序在 Java 平台运行时会被编译成字节码文件,然后可以在有 Java 环境的操作系统上运行。在运行文件时,Java 的解释器对这些字节码进行解释执行,执行过程中需要加入的类在连接阶段被载入到运行环境中。
(5)多线程
Java 语言是多线程的,这也是 Java 语言的一大特性,它必须由 Thread 类和它的子类来创建。Java 支持多个线程同时执行,并提供多线程之间的同步机制。任何一个线程都有自己的 run() 方法,要执行的方法就写在 run() 方法体内。
(6)分布式
Java 语言支持 Internet 应用的开发,在 Java 的基本应用编程接口中就有一个网络应用编程接口,它提供了网络应用编程的类库,包括 URL、URLConnection、Socket 等。Java 的 RIM 机制也是开发分布式应用的重要手段。
(7) 健壮性
Java 的强类型机制、异常处理、垃圾回收机制等都是 Java 健壮性的重要保证。对指针的丢弃是 Java 的一大进步。另外,Java 的异常机制也是健壮性的一大体现。
(8)高性能
Java 的高性能主要是相对其他高级脚本语言来说的,随着 JIT(Just in Time)的发展,Java 的运行速度也越来越高。
(9)安全性
Java 通常被用在网络环境中,为此,Java 提供了一个安全机制以防止恶意代码的攻击。除了 Java 语言具有许多的安全特性以外,Java 还对通过网络下载的类增加一个安全防范机制,分配不同的名字空间以防替代本地的同名类,并包含安全管理机制。
1.4 java运行环境
JDK 的全称(Java Development Kit Java 开发工具包) JDK = JRE + java 的开发工具 [java, javac,javadoc,javap 等], JDK 是提供给 Java 开发人员使用的,其中包含了 java 的开发工具,也包括了 JRE。所以安装了 JDK,就不用在单独 安装 JRE 了。
JRE(Java Runtime Environment Java 运行环境) JRE = JVM + Java 的核心类库[类],包括 Java 虚拟机(JVM Java Virtual Machine)和 Java 程序所需的核心类库等,如果想要运行一个开发好的 Java 程序, 计算机中只需要安装 JRE 即可。
总结
JDK = JRE + java开发工具
JRE = JVM + java类库
JVM = Java虚拟机
因此 我们需要下载JDK,地址如下:
https://www.oracle.com/cn/java/technologies/downloads/
下载安装后,进入bin目录(此版本jdk14)
bin 文件夹里是 可执行的二进制文件
lib 文件夹里是 库文件
config 文件夹里 是 配置文件
进入bin目录,我们将目光聚焦于javac.exe,java.exe
javac.exe java编译器,将java文件编译为class(字节码)文件,计算机可以识别class文件并执行。
java.exe 通过启动jre,加载特定的class文件
HelloWorld.java
public class HelloWorld{
public static void main(String[] args){
System.out.println("HelloWorld").
}
}
编写完成后,在cmd(命令行)中使用编译器与运行器,就可以执行java代码
"D:\Java\bin\javac" HelloWorld.java
"D:\Java\bin\java" HelloWorld.class
为了便于操作,我们将bin目录放入windows环境变量之中。
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。
这里简单点说,如果在cmd之中随便输一些词语,会提示不是内部或外部命令,也不是可运行的程序或批处理文件。
这其实就是字面意思,系统找不到你输入的词语对应的命令或exe(可执行)程序,系统去查了,没查到,那么怎么让他能查迟到呢?
在环境变量path路径下加入bin目录,则,系统在查询可运行程序是也会前往bin目录查询,
(有很多教程都配置了两三个,如%JAVA_HOME%\bin 这是因为系统认为%之间的JAVA_HOME为一个变量名,他会找这个变量的值,实际路径其实和上面一致)
这样,我们就可以直接输入
javac HelloWorld.java
java HelloWorld.class
2. 变量与计算
变量是一段实际连续存储空间的别名 ,实际上这个别名代表的是这段连续存储空间的首地址。 并且这段存储空间的大小是由数据类型决定的。
2.1 数据类型
由于存储单元中存放的数据内容大小不一样,导致所需存储单元的大小不一样,在Java语言中用数据类型加以描述。
java语言中数据类型分两大类,分别是基本数据类型,一种是引用数据类型。基本数据类型又名基本类型又叫内置数据类型,引用数据类型的实例又叫对象。
基本数据类型共8种,其他的都是引用数据类型。
数据类型 | 比特位(1字节B=8位b) | 数据 | 数值范围 |
---|---|---|---|
byte | 8位 | 有符号的以二进制补码表示的整数 | -128-127 |
short | 16位 | 有符号的以二进制补码表示的整数 | -215-215-1 |
int | 32位 | 有符号的以二进制补码表示的整数 | -231-231-1 |
long | 64位 | 有符号的以二进制补码表示的整数 | -263-263-1 |
float | 32位 | 符合IEEE 754标准的浮点数 | -3.4 E+38 ~ 3.4 E+38 |
double | 64位 | 符合IEEE 754标准的浮点数 | -1.7 E -308~1.7 E+308 |
boolean | 1位(规定一般32位处理,如果数组8位) | true or flase | |
char | 16位 | Unicode(无符号)字符 |
在计算机中,整数型使用二进制方式表示:而每一个整数型的第一个二进制都是作为正负符号。 0=正 1=负
- 基本数据类型在栈中进行分配,也就是说存储了实际的数值;
- 对象类型则是在堆中进行分配,它实际存存储的其实是引用地址;(但是数据的引用在栈中);
//基本数据类型定义 基本数据类型 变量名 = 初始值;
int a = 1;
double b = 5.0;
//对象定义 对象的定义方法其实有很多,有new(最常见)、反射、反序列化、第三方库、克隆
User user = new User();
//通过类.class的方式获取一个该类的Class实例
Class dogClass = Dog.class;
//通过调用newInstance()方法获取一个Dog的实例,该方法会返回一个Object类型的对象并且会抛出两个异常
Dog dog = (Dog) dogClass.newInstance();
//反序列化
Car car1 = new Car(175, "白色");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("car1.ser"));
Car car2 = (Car) ois.readObject();
System.out.println("car2:" + car2);
ois.close();
//JSON串转对象
VO vojs= JSONObject.parseObject(str,VO.class);
//克隆
ser2 a1 = new User2();
a1.setName("陈宇涛");
a1.setAge(20);
User2 a2 = a1.clone();
变量使用的注意事项
- 使用变量之前需要声明变量。
- 使用变量之前需要初始化。
- 变量名不能重复声明。
- 变量名由数字、字母、下划线以及$等组成,但数字不能开头
需要注意的是:
float a = 5.0
这段代码会报错,因为小数默认都是double,如果想要一个float,那么应该是
float a=5.0f
基本类型之间的传值是值传递, 对象之间的传值是则一直有所争议。
扩展 对象传值测试
关于java对象究竟是值传递还是引用传递,一直是众说纷纭,各自都有各自的理由,
值传递认为:是将将栈里存的引用(就是堆中的对象实例地址)复制了一份交给了方法体的局部变量,而不是直接传递
引用传递认为 :在调用函数时将实际参数的地址直接传递到函数中,而传递过来的地址还是与之前的地址指向同一个值,那么要是修改了这个参数,就会影响这个值的改变
最后,我选择了主流观点:对象之间的传值是值传递
,来看这么一段代码:
public static void main(String[] args) {
// TODO Auto-generated method stub
String str = "data1";
String str2 = new String("data2");
StringBuffer buffer = new StringBuffer("data3");
System.out.println("1 str:=" + str + " str2:=" + str2+ " buffer:=" + buffer.toString());
//1 str:=data1 str2:=data2” buffer:=data3
dealData(str, str2, buffer);
System.out.println("2 str:=" + str + " str2:=" + str2 + " buffer:=" + buffer.toString());
//2 str:=data1 str2:=data2” buffer:=data3123
}
private static void dealData(String str, String str2, StringBuffer buffer) {
str += "123";
str2 += "123";
buffer.append("123");
System.out.println("nerborn: str:=" + str + " str2:=" + str2 + " buffer:=" + buffer.toString());
//nerborn: str:=data1123 str2:=data2123 buffer:=data3123
}
我们将结果放在一起
1 str:=data1 str2:=data2” buffer:=data3
nerborn: str:=data1123 str2:=data2123 buffer:=data3123
2 str:=data1 str2:=data2” buffer:=data3123
str与str2,因为String内部的value数组是被final修饰的,所以在进行字符串拼接会创建一个新对象,也就是说 在dealData中,进行字符串拼接后就产生了一个新对象,也就是说这时候的str与str2已经指向了堆里一个新对象实例,自然是不会对原本的对象产生修改
接下来我们来看buffer,很多人都觉得buffer就是典型的引用传递,但其实不然,因为这个例子其实有问题,
我们可以换成这样
private static void dealData(String str, String str2, StringBuffer buffer) {
str += "123";
str2 += "123";
buffer = new StringBuffer("data3123");
System.out.println("nerborn: str:=" + str + " str2:=" + str2 + " buffer:=" + buffer.toString());
}
其余均不变,结果为
1 str:=data1 str2:=data2 buffer:=data3
nerborn: str:=data1123 str2:=data2123 buffer:=data3123
2 str:=data1 str2:=data2 buffer:=data3
相信很多小伙伴看到这里就已经明白了,buffer.append方法,本质上会修改对象实例中的value数组,所以看起来我改他也该,
而当我们选择new一个对象时,地址引用发生改变,原对象自然也不再改变,
所以我们可以得出:java对象是值传递,看起来是引用传递,是因为对象所属类中存在改变对象实例数据的方法
2.2 String
在介绍完基本数据类型之后,再简单介绍一下String类型。
String其实是一个不可变的char数组,
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 该值用于字符存储。 */
private final char value[];
在日常生活中,我们都是用一串连续的话去进行交流的,但char本身只能存储一个字符,因此,在代码中,char数组,也就是String的应用就非常频繁,因此,java进行了很多优化,现在,String本质是个引用数据类型,但平常使用,却和基本数据类型差不多。
String a = "全翁"
String b = "阿萨德"
String c = a + b;
System.out.println(c);//全翁阿萨德
总之:太常用了,就简化了。
2.3 类型转换
在实际应用中,经常需要在不同类型的值之间进行操作,这时就需要进行数据类型的转换。 数据类型转换有两种:
- 自动类型转换:编译器自动完成类型转换,不需要在程序中编写代码;
规则:从存储范围小的类型到存储范围大的类型。
具体规则:byte→short(char)→int→long→float→double.
例如:byte b= 100;int n = b;
注意:对于short、char两种类型而言,他们是相同级别的,因此,不能相互自动转换,但是可以强制类型转换 - 强制类型转换 :强制类型转换,也称显式类型转换,是指必须书写代码才能完成的类型转换。该类类型转换很可能存在精度的损失,所以必须书写相应的代码,并且能够忍受该种损失时才进行该类型的转换。
转换规则:从存储范围大的类型到存储范围小的类型。
具体规则为:double→float→long→int→short(char)→byte
例如:double d = 3.10;int n = (int)d;
扩展 为什么long可以自动转换为float
这是因为long和float的存储方式的不同
我们知道:long占8个字节 表示范围为2^63 —2^63-1
但float不同,
float:单精度浮点数 V=(-1)^s ∗ M ∗ 2^E
浮点数的32位不是简单的直接表示大小,而是按照一定的标准分配的(本质上就是科学计数法,因此存在一定的精度丢失)。
其中第1位,符号位,即S。【所以符号位1表示负数,0表示正数】
接下来的8位,指数域,即E,为了同时表示正负指数以及他们的大小顺序,这里实际上的数是E-127,比如如果是126,则这里存储的是126-127=-1,
剩下的23位,小数域,即M,M的取值范围为[0,1)或【1,2)。
如果正是因为指数域的存在,导致float范围发生了巨大的变化
我们可以来算一下float的最大值,
首先必须为正数,第一位为0
指数取最大值,因为1111111111我们不能占用,所以最大值为11111110 = 28-1-127=27
位数有23个1,则
最大为(2-2-23)*2127= (1-2-24)*2128 ≈ 2^128 = 3.40282367+E 38
简单来说float因为存储的方式,可以存储更多的数,存储的大小不是因为比特位数,而是因为存储的结构
扩展 进一步理解精度丢失
正是因为存储结构的不同,所以整数转浮点数肯定会存在精度丢失
int是准确值,而float是精确值,准确转精确当然会精度丢失
因此 :在进行货币等精确计算时应使用BigDecimal。
程序中如果对精度要求不是很高的情况,可以使用float。但精度要求高的情况,要尽量使用double。如果要求更高的精度,则应使用BigDecimal
为什么float被称为精确值,这就是因为尾数的存在,尾数默认23位,加上默认省略的1位就是24位,因此如果int类型的值在2^24以内,float是可以精确表示的,但是当超过这个数的时候就不一定能精确表示了。(就是科学计数法,一般记上三四位,后面直接省略了,因此在实际应用中,由于钱这个需要精确计算,所以钱是不会使用double的)
举个例子:
float 32 符号位 8位指数位(0-255) 23位尾数位
2.5 转二进制 10.1 科学计数法表示 1.01*2^-1
符号位 为正数 0
指数为 -1+127=126=01111110
小数位 01 后面补0 01000000000000000
那么最终结果为 0 01111110 01000000000000000
2.4 基本类型包装类
八大基本数据类型后,就是对象(引用数据类型),但在对象之中,也存在一些看起来就很眼熟的对象:如:Integer,Long,Float,Double。这些看起来很像基本数据类型,但他们其实是对象。
- 1、基本类型在Java的lang包中都有一个相应的包装类,如:Byte,Short,Character,Integer,Long,Float,Double,Boolean,通过这些包装类,我们就可以将基本数据类型包装成类对象。
- 2、包装类的构造方法:
所有包装类都可将与之对应的基本数据类型作为参数,来构造它们的实例,例如:Integer i=new Integer(1);
除Character类外,其他包装类可将一个字符串作为参数构造它们的实例,例如:Integer i=new Integer(“123”); - 3、注意事项:
Boolean类构造方法参数为String类型时,若该字符串内容为true(不考虑大小写),则该Boolean对象表示true,否则表示false;
当Number包装类构造方法(上面的8种包装类中除了Character和Boolean,都继承了Number)参数为String 类型时,字符串不能为null,且该字符串必须可解析为相应的基本数据类型的数据,否则编译通过,运行时NumberFormatException异常; - 4、所有的包装类内部都是final类型的基本数据,所以都是不可变的
扩展 自动装箱与拆箱
自动装箱和拆箱从Java 1.5开始引入,目的是将原始类型值转自动地转换成对应的对象。
在JDK1.5之前,基本类型转换为对象需要程序显示用包装类进行处理,有了装箱和拆箱后就自动了,程序就不需要显示处理了。例如:Integer num = 1;
-
1、什么是自动装箱和拆箱?
自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。 -
2、自动装箱拆箱要点
自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
自动装箱是将boolean值转换成Boolean对象,byte值转换成Byte对象,char转换成Character对象,float值转换成Float对象,int转换成Integer,long转换成Long,short转换成Short,自动拆箱则是相反的操作。 -
3、何时发生自动装箱和拆箱
自动装箱和拆箱在Java中很常见,比如我们有一个方法,接受一个对象类型的参数,如果我们传递一个原始类型值,那么Java会自动讲这个原始类型值转换成与之对应的对象。
例如:
ArrayList intList = new ArrayList();
intList.add(1); //自动装箱
intList.add(2); //自动装箱
自动拆箱(unboxing),也就是将对象中的基本数据从对象中自动取出。如下可实现自动拆箱:
Integer i = 10; //装箱
int t = i; //拆箱,实际上执行了 int t = i.intValue();
4、注意事项:
Integer.valueOf(int i) 这个方法,如下:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i); }
对于–128到127(默认是127)之间的值,Integer.valueOf(int i) 返回的是缓存的Integer对象(并不是新建对象,创建这个缓存是为了提高效率)
2.5 基础数据类型的计算
(1)算术运算符
+ - * / % ++ --
注意,所有比int小的数据类型,进行计算时,结果都会变成int,在int之上的数据类型进行加减时,结果会自动变成过程中的最高类型。包装类也同意遵循这个规则。
i++
,--i
其实相当于 i = i+1,他们的不同点在于,在执行这行代码时,++i先加1,在使用,i++,先试用,在+1;
int a=13;
System.out.println(a++);//先输出a 在+1
System.out.println(a);//输出a
System.out.println(++a);//先+1,在输出a
int c = 11;//此时 a=15 c=11
System.out.println(c = c + ++a);// a=15+ 1=16 c=11+16=27
System.out.println(c = c + a++);// c=27+16=43 a=16+1=17
System.out.println("a = " + a+",c = "+ c);
结果如下:
(2) 位运算符
& | ^ >> << 从左到右依次为 与 或 异或(10为1,01为1,11为0,00为0) 右移 左移
如:13&11=9 13|11=15 具体运算如下:13二进制1101,11二进制1011
1101
1011
& 1001 9
| 1111 15
^ 0110 6
左移与右移其实就是二进制整体移动,比如
5 00000000 00000000 00000000 00000101
5>>2 00000000 00000000 00000000 00000001 1 (整体右移2位,舍弃01,正数高位补0,负数高位补1)
5<<2 00000000 00000000 00000000 00010100 20 (整体左移,不管正负,低位补0)
-5原码 10000000 00000000 00000000 00000101
-5反码 11111111 11111111 11111111 11111010
-5补码 11111111 11111111 11111111 11111011
补码>>2 11111111 11111111 11111111 11111110
除符号位,其他全部取返,末尾加1,得
-5>>2 10000000 00000000 00000000 00000010 -2
-5补码 11111111 11111111 11111111 11111011
补码<<2 11111111 11111111 11111111 11101100
除符号位,其他全部取返,末尾加1,得
-5<<2 10000000 00000000 00000000 00010100 -20
-5补码 11111111 11111111 11111111 11111011
补码>>>2 00111111 11111111 11111111 11111110
注意,此时符号位为0,则原码反码补码均一样,则
-5>>>2 00111111 11111111 11111111 11111110 根据二进制规律,结果为2^30-1-1 10737418222
(3) 逻辑运算符
&& || ! 从左到右依次为 逻辑与 逻辑或 逻辑非
&&双方条件都必须满足,一旦第一个条件为假,直接输出false
||双方条件有一个满足即可,只有有一个为true,直接输出true
(3) 赋值运算符
+= -= *= /= =
i += 5 相当与 i = i + 5
(4) 三目运算符
条件 ? 结果1 : 结果2;
条件的结果一定是boolean ,只能是true或者false;
条件是可以复杂的
boolean condition1 = 5 > 3;
boolean condition2 = 5 > 3;
boolean condition3 = 5 > 3;
int num = (condition1 && (condition2 || condition3)) ? 1 : 2;
3.流程控制语句
1. 顺序控制
顺序控制就是程序会由上而下,由左及右依次逐行执行,就和我们写文章一样。
2. 分支控制语句
常见的分支控制语句有if语句和switch语句
if (alcohol >= 80) {
System.out.println("兄弟,你这是醉驾...");
} else if (alcohol >= 20) {
System.out.println("兄弟,你这是酒驾...");
} else {
System.out.println("兄弟,抓错了");
}
switch (operator) {
case 1:
System.out.println("请输入修改后的值:");
String setvalue = scanner.nextLine();
util.setPDU(oid, setvalue);
break;
case 2:
System.out.println("正在进行get查询");
util.getPDU(oid);
break;
case 3:
System.out.println("正在进行getNext查询");
util.getNextPDU(oid);
break;
case 4:
System.out.println("请输入查询个数:");
int maxRepetitions = scanner.nextInt();
util.getBlukKPDU(oid, maxRepetitions);
break;
case 0:
System.out.println("即将退出此次oid操作");
util.close();
running = false;
break;
default:
System.out.println("无效的操作符。");
注意:
- witch和if有点像,但凡switch能解决的问题,if也能解决。
- switch后不能是long型,但可以 byte shor int 也可以是String 。
- switch的default只能有一个。
- switch不加break会继续进入下一个判断。
3.循环语句
循环语句一般有两个,分别是for与while
for (int i = 0; i <= 10; i++) {
System.out.print(i * i + " ");
}
//监听redis中的队列TOPIC_HIGH_SMS
public class HighSmsListener extends Thread {
@Override
public void run() {
//TODO 监听TOPIC_HIGH_SMS队列,如果有消息则调用短信发送工厂发送实时短信
//监听 TOPIC_HIGH_SMS 发送
while (true){
log.debug("队列{}正在监听中",queueKey);
//SmsSendDTO -->string
String message = (String) listOps.rightPop(queueKey, popTimeout, TimeUnit.MILLISECONDS);
if(StringUtils.isNotBlank(message)){
log.info("{}收到消息了:{}",queueKey,message);
//发送
smsFactory.send(message);
}
}
}
}
我们要明白:
- 循环是一个无限的行为。
- 我们有时需要让循环一直持续下去:比如监听器,他需要一直等待消息,在消息发出后,立刻捕获。
- 更多的时候我们需要一个合适的时机退出循环,其实定义变量,条件判断,变量累加,都是为了找到这个时机。
【break和continue】
continue
:跳出本次循环,继续执行下一个循环。
break
: 跳出全部循环
4.数组与算法
4.1数组结构
在计算机中,我们有这样一种数据结构能帮助我们把想同类型的数据统一聚拢放在一起,他就是【数组】,数组中的元素被存储在一段连续的内存空间中。前面有提到,我们频繁使用的String其实就是一个char类型的数据。
数组的定义方式一般有
定义:
int[] nums;
初始化:
nums = new int[3];
因此有一种写法
类型[] 名字 = new 类型[长度];
int nums[] = new int[5];
赋值:
nums[0] = 1;
nums[1] = 4;
nums[2] = 3;
// 直接定义初始化并赋值
int[] nums = {1,2,3};
// 这样写也行
int nums[] = new int[]{1,2,4,5};
- 数组不初始化不分配空间,初始化必须指明数据类型与个数,初始化后不进行赋值的话为默认值。
- 数组中有一个属性,可以获取数组的长度 length,他的地址索引为0--(length-1),如果给nums[n]赋值将会报错ArrayIndeOutOfBoundsException。
- 数组一旦建立,长度不能改变。
- 数组是一个连续的线性表,因此查询效率比较高,如果删除中间元素,那么后续元素全部往前,因此一般不建议对数组进行频繁的添加与删除操作。
- 数组里边可以是基本类型,也可以是引用类型。
4.2数组遍历
由length的存在,因此数组的遍历一般使用for循环,也可以使用增强for循环,流。
for(int i=0;i<a.length;i+++){
Syetem.out.println(a[i]);
}
//增强for循环
for (int num : a) {
System.out.println(num);
}
//利用Java 8中引入的Stream API,可以以函数式的方式遍历数组并进行处理。
Arrays.stream(array).forEach(num -> System.out.println(num));
4.3数据倒装
将数组内部的元素进行倒装,即1,2,3,4,5,6的顺序变成6,5,4,3,2,1
有两种思路
- 定义一个等长的新数据,倒着遍历
int[] nums = new int[]{1,2,3,4,5,6};
int[] temp = new int[nums.length];
for (int i = nums.length - 1; i >= 0; i--) {
temp[i] = nums[nums.length - 1 - i];
}
nums = temp;
for (int i = 0; i < nums.length; i++) {
System.out.println(temp[i]);
}
- 申请一个临时变量
// 定义原始数组
int[] nums = new int[]{3,4,6};
// 交换反转
for (int i = 0; i < nums.length/2; i++) {
int temp = nums[nums.length - 1 - i];//保存
nums[nums.length - 1 - i] = nums[i];
nums[i] = temp;
}
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
4.4数组查询
一般查询,我们可能会想要使用for循环。
int target ;
for (int i = 0; i < salary.length; i++) {
if(salary[i] == 5){
res = salary[i];
}
}
System.out.println(target);
在处理大规模数据集或需要频繁查询的场景下,对数组进行高效的查询操作是至关重要的。传统的线性搜索方法可能会导致低效和耗费大量时间的问题,而算法则可以帮助我们提高查询效率和准确性。因此,我们会选择使用算法。
4.5算法
4.5.1排序思想
大部分算法中,为了保证高效的查询,都有一个前提条件按:有序数组。
因此,有三种主要思想
- 通过相邻元素的比较和交换,将最大(或最小)的元素逐渐“冒泡”到数组的一端。典型算法:
冒泡排序
,快速排序
。 - 每次从待排序的元素中选择最小(或最大)的元素,并放到已排序部分的末尾。典型算法:
选择排序
,堆排序
。 - 将待排序的元素逐个插入到已排序部分的合适位置中。典型算法:
插入排序
,希尔排序
这些算法在学习数据结构时,应该见过。此处暂时就不扩展了,只介绍一下java一种简单的排序方式,我们可以使用Arrays.sort
。
int[] array = {6, 2, 8, 4, 1, 9};
System.out.println("排序前的数组:" + Arrays.toString(array));
Arrays.sort(array);
System.out.println("排序后的数组:" + Arrays.toString(array));
注意:Arrays.sort
方法是针对基本数据类型的数组进行排序的。如果你想对其他类型的数组进行排序,例如字符串数组或自定义对象数组,你需要确保这些类型实现了 Comparable
接口或者提供了自定义的比较器(Comparator
),以便排序算法可以正确比较元素的顺序。
比如:
Comparator
其实是一个比较器,这种方法的好处是,实现时不需要对实体类进行修改,直接写一个内部类就行
Arrays.sort(数组,new compartor(){
@Override
public int compare(String o1, String o2) {
//返回“负数”,意味着“o1比o2小”;返回“零”,意味着“o1等于o2”;返回“正数”,意味着“o1大于o2”
return o1.equals(o2)?1:-1;
}
})
Comparable
接口是一个排序接口,通过重写CompareTo方法,指定排序原则,这里给一个实例:
public class Student implements Comparable<Student> {
String name;
@Override
public int compareTo(Student student) {
return student.name.equals(this.name) ? 1 : -1;
}
public Student(String name) {
this.name = name;
}
public Student( ) {
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("全翁");
students[1] = new Student("阿萨德");
students[2] = new Student("周星驰");
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// Arrays.stream(students).forEach(student -> System.out.println(student.toString()));
}
4.5.2查找思想
怎么查找,我们之前已经讲过了,一个个遍历,找到拿出,这种方法叫顺序查找
,那有没有更加高效的方法呢?如果数组本身无序,那确实没事好的办法,但是如果数组有序那就不一样了。
- 二分查找(Binary Search):对于有序数组,二分查找是最常用的算法之一。二分查找将数组不断分割为两半,并针对中间元素与目标值进行比较。根据比较结果,将查找范围缩小为剩余部分的一半,直到找到目标元素或确定不存在目标元素。二分查找的时间复杂度为 O(log n)。
- 插值查找(Interpolation Search):插值查找也适用于有序数组,类似于二分查找,但是它通过目标元素与数组中元素的大小关系,根据估计的位置进行插值,以更快地定位到目标元素。插值查找在数据分布均匀的情况下,可以比二分查找更快地找到目标元素。插值查找的最好时间复杂度为 O(log log n),最坏情况下为 O(n)。
- 斐波那契查找(Fibonacci Search):类似于二分查找和插值查找,使用斐波那契数列将查找范围划分成两部分,并选择黄金分割点进行比较。时间复杂度为 O(log n),适用于对有序数组进行查找。
当然,如果数组无序,也不代表没有办法:
- 哈希查找(Hash Search):基于哈希表的查找算法,通过将数组元素映射到一个哈希表中,然后根据键值快速找到对应的值。哈希查找的时间复杂度为 O(1),但是需要额外的空间来存储哈希表。
正常学习中,使用for循环遍历就可以处理这些问题,因此算法的使用并不是很大,但在企业级应用中,大量数据构建了一个庞大的数据结构,算法的重要性就体现出来了,企业中也会有专门的算法工程师来处理这些问题,因此,对于后端开发,只需要了解基本的算法就可以。
算法是一个很大的篇章,我现在也不适合你擅长,此处就不继续扩展了。
4.6数据扩容
数据一旦才创建,大小就不能改变,申请的内存只有被jvm回收之后才可以被重新利用,因此,数组的扩容,本质上还是创建了一个新数组。
// 定义原始数组
int[] nums = new int[]{3,4,6};
// 定义一个新的临时数字
int[] temp = new int[6];
// 讲原始数组的数据全部拷贝到临时数组
for (int i = 0; i < nums.length; i++) {
temp[i] = nums[i];
}
// 让原始数组的引用指向临时数组,感觉上就像原始数组被扩容了
nums = temp;
for (int i = 0; i < nums.length; i++) {
System.out.println(temp[i]);
}
如果多个用户同时访问系统,向该数据中分别执行添加数据,删除数据,修改数据,查询数据,由于执行的先后顺序,每次的查询结果可能都不一样,这就是我们常说的
线程安全
问题。
扩展:使用二维数组实现杨辉三角
import java.util.Arrays;
/**
* @ClassName YanghuiTriangle
* @Description TODO
* @Author yfmw
* @Date 6/7/2023 下午3:51
* @Version 1.0
*/
public class YanghuiTriangle {
public static void main(String[] args) {
int[][] triangle = new int[10][];
for (int i=0;i<triangle.length;i++){
triangle[i] = new int[i+1];
triangle[i][0] = 1;
triangle[i][i] = 1;
for(int j=2;j< i+1;j++){
triangle[i][j-1] = triangle[i-1][j-2]+triangle[i-1][j-1];
}
}
Arrays.stream(triangle).forEach(a -> System.out.println(Arrays.toString(a)));
}
}
5.面向对象
1.面向过程
在C语言中,我们使用的就是面向过程,面向过程就是指,将分析出解决问题的步骤,然后一步一步实现这个步骤。比如:一个人设计游戏,所有事情都只能自己一个人去做,人物模型,战斗系统,剧情设计,音乐设计,场景渲染,必须一个人自己都干了。
2.面向对象
面向对象可以说是更高级的抽象,他会将每个对象的行为抽象出来,负责模型的只设计模型,战斗系统有专门的策划,剧情有专门的文案.....
面向过程强调的是完成既定目标需要完成哪些步骤,总结完步骤之后,把所有步骤走完也就达到了既定目标了,所以一个目标就是各个步骤的糅合的结果
面向对象强调的是完成既定目标需要哪些条件(比如需要什么物品),之后逐步满足各个条件之后,最后再进行组装合并,达到既定目标。
在我看来面向对象其实就是分而治之,将一件很复杂的事拆分成多个简单的事,然后交付给对应的对象去做(这个其实也就是面向过程),最后将他们的结果进行集成(组合),就得到了我们想要的东西。就比如流水线造车,汽车的所有零件在各个工厂先造好,然后运到组装厂进行组装,得到一辆车。
3.java类结构
public class Student{
public String name;
public int scope;
public Student( ) {
}
public Student(String name, int scope) {
this.name = name;
this.scope = scope;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", scope=" + scope +
'}';
}
一个普通的java类如上,第一行Class Student
表示这是一个类,类名(类的名字)叫做Student,类的结构由成员变量与成员方法构成,String name
int scope
就是成员变量,String toString()
就是成员方法,成员方法的()中,用于定于局部变量,如果局部变量名与成员变量名重复,那就以局部变量为准,此时如果想使用成员变量,需要使用this,this代指对象实例,因此this.变量名,访问的是这个对象的成员变量。
注意:一个方法只有在调用的时候,才能明确方法中的【this】具体指向哪个实例对象。
public class 类名{
类型 变量名;//成员变量
int scope;
类型 方法名()
String toString() {//成员方法
return "Student{" +
"name='" + name + '\'' +
", scope=" + scope +
'}';
}
返回类型 方法名(类型 局部变量名)//成员方法
void setScope(int scope) {
this.scope=scope;
}
返回类型指的是最后这个方法需要返回什么,void代表没有返回值,如果想返回一个int类型,则
/**计算阶乘*/
public int factorial(int num) {
int sum=1;
if(num<0) {
throw new IllegalArgumentException("需要计算的参数必须为正数!");//抛出不合理参数异常
}
for(int i=1;i<=num;i++) {
sum*=i;//sum = sum * i
}
return sum;
}
在这个方法中,因为返回类型为int,所以在最后,return sum;一旦return,则退出方法,因此一般return后面都是没有代码的,但如果是分支控制语句,如if语句,那么需要在分支末尾加入return。如:
public String sout(int flag){
int number=(int)(Math.random()*100);//生成一个[10,100]范围内的随机数
if(flag > number){
return "猜大";
} else if(flag < number){
return "猜小";
} else {
return "恭喜您,猜中了";
}
}
在实际使用中,我们会将人物抽象成一个一个类,如学生类student,这些类就是实体类,因为这些实体类会被频繁地使用,因此,java中,实体类有一些快速设计
package com.yfmw.Pojo;
/**
* @ClassName User
* @Description TODO
* @Author yfmw
* @Date 20/10/2022 下午3:37
* @Version 1.0
*/
public class User {
int id;
String name;
String sex;
//负责打印id
public int getId() {
return id;
}
//负责修改id
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
//构造方法,默认会有一个无参(没有参数)的构造方法,我们可以手动写(生成)一个有参构造方法
public User(int id, String name, String sex) {
this.id = id;
this.name = name;
this.sex = sex;
}
public User() {
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
'}';
}
}
setXxx负责修改对应的成员变量,getXxx负责打印对应的成员变量,而构造方法是一个特殊的方法,我们一般在创建对象时,是
User user = new User();
user.setId(1);
user.setName("qwe");
user.setSex("男");
这么写会比较繁琐,所以我们可以再对象初始化的时候,直接赋值,也就是
User user = new User(1,"qwe","男");
这么写的前提就是要有public User(int id, String name, String sex)
,构造方法是一个特殊的方法,他没有返回类型(void也不能写),并且他的方法名必须与类名一致,一般,每个类都有默认的无参构造方法,在编译时,会自动加上,但如果你重写了构造方法,加入了有参构造方法,他就不会再加上无参构造方法,因此,一般建议,有有参构造方法,那就也要把无参构造方法写出来,防止这种不必要的报错。
4.面向对象三大特性
- 封装
封装:把一个类的属性私有化,同时提供一些可以被外界访问的属性的方法。
简单点说就是将数据与行为分离。
java中提供了一些权限修饰符
作用域 | 当前类 | 同package | 子孙类 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
friendly( default ) | √ | √ | × | × |
private | √ | × | × | × |
基本这四个修饰符,我们就可以控制对象权限,比如:
private与public set,我们不允许外部程序随意修改系统内数据,所以我们将数据私有化,程序内部通过公有的set方法,变相去修改我们的数据,而外部程序无法调用这个set方法,就保护了我们的数据安全。
因此我们有一个规定:所有的属性必须私有化,使用setter和getter赋值或者取值。
- 继承
这是一个新名词,在java里面有父类与子类之称,子类通过extends继承父类,通过继承,子类可以获取父类被public与protected修饰的变量与方法,也就是说,子类通过继承获取父类的属性和行为。
注:一个类只能有一个父类,如果没有指定,则默认继承Object类。
Object类有许多方法,:
getClass、HashCode、equals、toString,clone,wait,notify,nofityAll,finalize,这些方法都很重要。
- 多态
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。
这个单看概念可能比较晕,我们来看他是如何实现的:
我们再看他的实现:方法重写与方法重载。
方法重载:一个类中可以有多个同名方法的存在,但他们的参数列表必须不一样。比如:查询,可以传一个参数,也可以有两个参数。(虽然现在不会这么写,但还是可以这么举例)
方法重写:如果一个类继承了类(或者重写了一个接口),那么他可以对父类(接口)的方法进行重写,重写后的方法会覆盖原来的方法。比如:在equals方法,在Object中使用==来进行比较,但在String中,他会去比较他的成员变量。
先介绍下==与equals方法:
对于基本数据类型:可以使用==是比较值,equals方法他不能使用。
对于对象: == 是比较自己所对应的地址,equals也是默认==,原因如下:
所有类都默认继承Object类,在Object之中存在方法equals,代码如下
public boolean equals(Object obj) { return (this == obj); }
因此,对于对象来说,默认==与equals没有区别,但对于我们使用者来说,我们其实不关注他的存储地址,比较关心他的具体值,因此,我们一般会重写equals方法。有些类也重写了,如String,Date、Integer,因此实际比较的是值;
String源码下的equals方法
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
总结:先比地址,在比长度,最后遍历欸个比较,不妨看看如下代码:
public static void main(String[] args) { // TODO Auto-generated method stub int n=3; int m=3; System.out.println(n==m); //基本类型值比较,true String str = new String("hello"); String str1 = new String("hello"); String str2 = new String("hello"); System.out.println(str1==str2); //对象引用地址比较,false str1 = str; str2 = str; System.out.println(str1==str2); //对象赋值后引用地址比较,true System.out.println(str1.equals(str2));//String的equals方法重写了,比较的是值,true StringBuffer sb = new StringBuffer("123"); StringBuffer test = new StringBuffer("123"); System.out.println(sb == test); //对象引用地址比较,false System.out.println(sb.equals(test));//StringBuffer的equals方法没有重写,比较内存地址,false }
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性.。通过多态,我们每次有新的需求,修改代码时就不必去原有的代码上去修改,通过继承父类(或抽象类或接口)可以直接获取原来的行为,然后通过重写或重载来提高代码的可扩展性。
5.String详解
5.1 String
我们已经知道了一个类的基本结构,那么是时候去详细学习最常见的类:String。
先贴源码(部分):
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** 该值用于字符存储。 */
private final char value[];
/** 缓存字符串的哈希码 */
private int hash; // Default to 0
/**
* 初始化一个新创建的String对象,使其表示一个空字符序列。请注意,由于字符串是不可变的,因此不需要使用此构造函数
*/
public String() {
this.value = "".value;
}
/**
*初始化一个新创建的String对象,使其表示与参数相同的字符序列;换句话说,新创建的字符串是参数字符串的副本。除非需要original *的显式副本,否则不需要使用此构造函数,因为字符串是不可变的。
*
*参形:原始 - 一个String
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
/**
*分配一个新的String以便它表示当前包含在字符数组参数中的字符序列。字符数组的内容被复制;随后对字符数组的修改不会影响新创建的 *字符串。
* 形参:
* value – 字符串的初始值
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
/**
* 分配一个新String ,该字符串包含来自字符数组参数的子数组的字符。 offset参数是子数组第一个字符的索引, count参数指定子数 *组的长度。子数组的内容被复制;随后对字符数组的修改不会影响新创建的字符串。
*形参:
* value – 作为字符源的数组
* offset - 初始偏移量
* count - 长度
*
*抛出:IndexOutOfBoundsException – 如果offset和count参数索引字符超出value数组的范围
*
*/
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
- String是一个对象,不属于8种基本数据类型,因为对象的默认值是null,所以String的默认值也是null;
- 但它又是一种特殊的对象,有其它对象没有的一些特性
例如:
-
new String()和new String(“ ”)都是申明一个新的空字符串,是空串不是null
-
String="abc"与new String("abc")是不一样的:
String str1 = "abc"; // 在常量池中 String str2 = new String("abc"); // 在堆上
关于String是不可变的
String内部是private final char value[]; 因此String的实例一旦生成就不会再改变了,即修改String的值会产生临时变量。关于final,以后在解释,这里只需要知道,被final修饰后,就不会再变了。如果想变,此时就只能使用反射。
所以代码中如果需要经常修改String的值,建议用StringBuffer或者StringBuilder。
5.2 String常用方法
- indexOf方法:字符串查找,在字符串中查找子字符串出现的位置,如过存在返回字符串出现的位置(第一位为0),如果不存在返回 -1。
- replace 方法:字符串替换。
public class test {
public static void main(String args[]){
String str="Hello World,Hello Java.";
System.out.println(str.replace('H','W')); //替换全部
System.out.println(str.replaceFirst("He","Wa")); //替换第一个遇到的
System.out.println(str.replaceAll("He", "Ha")); //替换全部
}
}
-
String的split方法 切割字符串,指定分隔符将字符串分割为数组,但是该方法在字符串很大的时候有性能瓶颈,所以如果把String按照某种分隔符拆分成String[]数组时,需要注意String的大小。
-
substring:截取字符串,通过制定起始索引(与结束索引)来截取字符串.
-
toUpperCase:全部大写
-
toLowerCase:全部小写
5.3 String的内存结构
String存储如下
String="abc" 当直接赋值时,字符串“abc”会被存储在字符串常量池中,只有1份,此时的赋值操作等于是创建0个或1个对象。
如果常量池中已经存在了“abc”,那么不会再创建对象,直接将引用赋值给str1;
如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。
String s0="abc";
String s1="abc";
String s2="a" + "bc"; //如果是常量进行字符串拼接时优化器会直接将拼接完成后的字符串,然后检验字符串常量池,
//也就是说,此时这行代码实际上并没有创建对象,只是栈上多了一个引用
System.out.println( s0==s1 ); //返回true
System.out.println( s0==s2 ); //返回true
那么,通过new String(“abc”);的形式又是如何呢?答案是1个或2个。
当JVM遇到上述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给str2。此过程创建了2个对象。
当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,并指向,此过程只创建了1个对象。
总结:
JVM会先在堆中建立一个空字符串,随后检测内存中是否有括号中的字符串,若有,直接修改空字符串,若无创建一个新的字符串并通过该字符串修改空字符串
String s0 = "abc"; //s0直接指向字符串常量池中的abc,
String s1 = new String("abc"); //s1指向队中的String实例,String实例指向字符串常量池中的abc,
String s2 = "a" + new String("bc");//s2指向堆中的String实例,注意此时是对象拼接而成的,
System.out.println(s0 == s1);//false
System.out.println(s0 == s2);//false
System.out.println(s1 == s2);//false
标签:Java,String,int,基础,System,println,java,out From: https://www.cnblogs.com/yfmw/p/17545353.html关于S1与S2,详细解释一下:
s1指向堆中的String实例,String实例指向字符串常量池中的abc,
s2则涉及到带到对象的字符串拼接,如果字符串拼接中包括字符串,那么会隐式创建一个StringBuilder对象,然后使用append方法将他们拼接到一起,最后执行toString方法,生成新的String对象(最后拼接成的字符串其实是不会放进字符串常量池的)
StringBuilder的toString方法如下
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
所以s1与s2不一致
这里还要提及String的一:个方法intern:
存在于class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;
当一个String实例apache调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,
如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于apache的字符串并返回它的引用;参照如下代码:
public static void main(String[] args) { // TODO Auto-generated method stub String s0= "apache"; String s1=new String("apache"); String s2=new String("apache"); System.out.println( s0==s1 ); System.out.println( "**********" ); s1.intern(); //s1调用intern方法后,因为没有赋值,所以没有改变引用 s2=s2.intern(); //把常量池中"kvill"的引用赋给s2 System.out.println( s0==s1); //false System.out.println( s0==s1.intern() ); //true System.out.println( s0==s2 ); //true }
必须注意的是:不是“将自己的地址注册到常量池中”了,而是在常量池中增加一个Unicode等于apache的字符串并返回它的引用