title: Java基础 date: 2024/7/15 tags: - java categories: - Java
Java
windows系统下的cmd常用命令
1.切换盘符(从c盘切换到d盘,d盘切换到c盘) 切换到d盘:D: || d: 切换到c盘:C: || c: 2.查看当然路径所在的文件夹下含有的文件及文件夹 dir 3.进入某文件夹目录 cd '文件夹的目录路径' 比如 'cd D://10.6' 进入d盘下的名为10.6的文件夹 注意该命令在windows系统下,进入文件夹目录只能进入当前所在盘的文件夹下,跨盘是进不去的,需要先切换到所在盘,再执行命令 cd './某文件夹' 进入到当前文件夹下的某文件夹 其中的'.'代表的是当前文件夹下的意思 cd '../某文件夹' 进入到上一层文件夹下的某文件夹 其中的'..'代表的是上一层文件夹的意思,你可以反复使用'..',比如cd '../../../某文件夹' cd '..' 退回到上一级目录 4.创建文件夹 mkdir '某文件夹' 在当前文件夹下创建一个文件夹为某文件夹的空文件夹 5.清屏 cls 将当前的cmd窗口显示出来的内容清理干净 6.退回到盘符目录 cd '/' 7.退出关闭命令行窗口 exit 关闭当前cmd窗口
环境变量的配置
1. 为什么要配置环境变量
在cmd命令行运行窗口中,如果想运行或执行某程序,比如'QQ.exe'程序,那么我们需要进入到'QQ.exe'所在的文件夹,再运行'QQ.exe'就能运行qq程序
先找到'QQ.exe'所在文件夹,可以看见我的'QQ.exe'是在'D:\dev\more\software\QQ'文件夹下的
那么在命令行窗口进入该文件夹,并且执行'QQ.exe'
这时qq程序图形化界面就打开了,和平时我们直接点击桌面的快捷方式结果是一样的
那么如果我们没在'QQ.exe' 所在的文件夹在执行'QQ.exe'命令,系统就会报错,报找不到'QQ.exe'可执行文件
比如我们在qq文件夹的上一级文件夹下执行'QQ.exe'命令,可以看见系统是报找不到可执行命令的
所以如果我们想要系统不报错,并且可以运行qq程序成功,以及后期我们cmd窗口'cd'到任何文件夹下,就能执行成功qq程序
我们就需要配置环境变量,来告诉系统,我们的'QQ.exe'是在文件夹系统下的哪个位置,这样系统才能找到'QQ.exe',并且执行它
配置了环境变量后:当你在任意的文件夹目录下,执行'QQ.exe'命令,系统会先在当前所在的文件夹下找是否存在'QQ.exe',如果存在就找到了,运行即可,否则会在环境变量中的列表(里面装的就是文件路径)中,从上到下,依次进入每个文件夹路径的文件,去该文件夹下找'QQ.exe',找到了就不在往后找了,如果把环境变量中的所有文件夹路径的全部找完都没找到'QQ.exe',那么就找不到了,系统就会报找不到可执行程序或命令了
下面讲解Java的环境变量的配置
2. Java环境变量的配置
方式一:直接将jdk的bin目录路径直接复制粘贴到系统变量path中即可,但这样做不规范,不便于扩展,所以建议使用第二种方式进行配置
方式二(推荐):先在环境变量中创建一个新的变量JAVA_HOME,然后将路径配置到jdk的bin目录的上一级即可
然后再在path变量中引用刚才配置的JAVA_HOME变量,再path中新增一行 %JAVA_HOME%/bin 即可
这样做后,后期可以在统一在一个jdk文件夹内存储不同版本的jdk,后期如果想切换本机jdk版本,只需要将jdk的JAVA_HOME的路径切换到需要使用的jdk版本的路径即可。
JDK和JRE
1. JDK的文件目录
javac是jdk提供的编译工具,它会将我们编写的java代码编译变成class文件,然后后期会将该class文件交给虚拟机运行。
java是jdk提供的一个工具,作用是用来运行已经编译后的class文件的。
2. JDK与JRE的区别
JDK是java的开发工具包,里面包含了jvm虚拟机、核心依赖包、以及一些开发工具,比如javac编辑工具,java运行工具,jdb和jhat等
而JRE是将单纯运行java程序必需要的工具等从JDK中抽离出来了,当我们写好了代码想将该程序给用户运行,只需要给用户安装JRE即可,没必要再安装JDK格外占用内存了。原因是:像JDK中的javac是往往用户单纯的运行程序是完全不需要的,因为我们会将程序提前编译成class文件传给用户,用户只需要用JRE直接运行class文件即可
3. java的跨平台运行原理
java的跨平台运行原理主要依赖于虚拟机,javac会将我们编写好的java代码编译成class文件,该class文件操作系统并不能直接运行的,是会给虚拟机运行。java给用户提供了不同操作系统的JRK,你可以选择自己对应系统的JDK,比如windows,然后该JDK中自带的就是对应windows版本的虚拟机,当该虚拟机运行class文件时,它会将该class文件翻译为windows系统能够执行的代码,再交给windows系统执行。所以我们编写的同一份java代码,如果想交给其它系统执行,只需要下载对应系统的JDK即可。
注释和关键字
1. 注释
注释在代码编译成字节码文件时,字节码文件中不会有注释信息(注释内容不会参与编译和运行,仅仅是对代码的解释说明)
常用的注释有3种:
1.单行注释 // 注释信息 2.多行注释 /* 注释信息1 注释信息2 注释信息3 */ 3.文档注释 /* * 文档注释 * */ 注释不要进行嵌套
2. 关键字
在Java中,被Java赋予特殊含义的英文单词为关键字,比如final,class等等,它们都是小写的,在idea中会被高亮显示。
java中所有的关键字 abstract assert boolean break byte case catch char class const(未使用) continue default do double else enum extends final finally float for goto(未使用) if implements import instanceof int interface long native new null package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while
数据类型
Java中数据类型分为基本数据类型和引用数据类型,下图为基本数据类型,除开了下图的基本类型外,其它的数据类型都是引用数据类型,比如String,数组,还有自己定义的类,这些都是引用数据类型。
1. 基本数据类型
注意事项:
-
byte,short,char参与运算都会将运行类型转为int,所以最终接收数据的类型是int
但 += -= *= /= %= 是特例,因为它们的底层实现实际上是在运算结果前面加上了一个强转类型强转
byte a = 1, b = 2; byte c = 0; c += a + b; // 这个是正确的 c = a + b; // 这个是错误的,编译程序时,语法检测都过不了
-
定义和使用long类型和float类型变量时候,数据值需要加上L或F作为后缀(实际l和f都可以,但建议还是用大写字母)。
2. 基本数据类型和引用数据类型的区别
2.1 基本数据类型和引用数据类型的本质区别是:定义该数据的变量的值是存储的实际数据是什么(基本数据类型存储的是真实的变量值,但引用类型存储的是该变量值存储在堆空间的地址)。
比如int a = 3,该变量a的值是3,你直接打印,打印出来的就是3;但如果是引用类型就不一样了,比如int[] a = new int[10],该变量a存储的就不是单纯的变量值了,存储的是该变量数据在堆空间中存储对应的地址值,通过该地址,就是找到变量a实际真实的值。你直接打印a,打印出来的是a的堆内存空间地址(以16进制显示)。这是本质上的区别。
所以这也是为什么字符串(实际只要是引用类型都不能单纯的用==直接比较)比较不能单纯的使用==去进行比较,因为如果这样做,那么比较的则是字符串的地址值,而不是字符串的值。这引出一个知识点,引用数据类型如果做比较,需要重写它继承父类Object的equals方法,在Object类中,equals方法比较的是地址。
2.2 定义和使用变量时存储数据涉及到的堆空间和栈空间:存储数据涉及到的在定义对象的时候,在jvm虚拟机中实际上是发生了以下事情,虚拟机会将内存划分成多个区域,便于更好的管理内存。我们目前主要看栈空间和堆空间。定义变量都是在栈空间中的,如果是基本数据类型,则在栈空间中直接存储该数据值,如果是引用数据类型,则需要格外在堆空间中new出一片区域来存储该数据,而该变量在栈空间中存储的就是该变量在堆空间中存储数据的这片地址值。
算术运算
1. 算术运行符
这个比较简单,就不多做简绍了
注意:其中运算涉及到小数(double,float)运行,则最终结果可能不准确,会有精度损失,如果对精度要求比较高,应该使用大整数类型(BigInteger) 和 大浮点数类型(BigDecimal);
2. 隐式类型转换和强制类型转换
在java中,数据运算时,数据类型不一致是不能进行运行的,需要将数据类型转一致,才能进行运算。
2.1 隐式转换
在进行运算时候,如果两类型不一致,取值范围不一致,会将取值范围小的类型向上转型,转成取值类型大的,这样精度不会损失。
下图是类型转化的顺序,但值得注意的是,byte short chart进行运算时,都会将类型转成int,一般最终接受结果的类型也是int。
2.2 强制类型转化
强制类型转化可能是会出现精度损失的
3. 运算符中的“+”操作
各种情况的运算结果
4. 关系运算符
5. 逻辑运算符
6. 短路运算符
7. 三元运算符
8. 自增和自减
在计算机中,计算机底层运算都是以补码的形式运算的
判断和循环
1. if语句
public static void main(String[] args) { boolean a = true; boolean b = true; if(a) { System.out.println("如果a条件判断为真,则打印此语句"); } if(a) { System.out.println("如果a条件判断为真,则打印此语句"); } else { System.out.println("否则打印此语句"); } if(a) { System.out.println("如果a条件判断为真,则打印此语句"); } else if (b) { System.out.println("否则b条件判断为真,则打印此语句"); } if(a) { System.out.println("如果a条件判断为真,则打印此语句"); } else if (b) { System.out.println("否则b条件判断为真,则打印此语句"); } else { System.out.println("否则打印此语句"); } }
2. switch语句
public static void main(String[] args) { int a = 5; switch (a) { case 1: System.out.println("a的值是1"); break; case 2: System.out.println("a的值是2"); break; case 3: System.out.println("a的值是3"); // break; break 表示不再执行后面的代码了,如果没有break,将会将case4的语句一起执行 case 4: System.out.println("a的值是4"); break; default: System.out.println("上面的语句都没用命中"); } }
3. 循环语句
public static void main(String[] args) { // for循环 for (int i = 0; i < 10; i++) { System.out.println(i); } // while循环 int a = 0; while (a < 10) { System.out.println(a); ++a; } // do... while 循环 int b = 0; do { System.out.println(b); b++; } while (b < 10); // 无限循环 for(;;) { System.out.println("无限循环"); } while (true) { System.out.println("无限循环"); } do { System.out.println("无限循环"); } while (true); }
一般知道循环次数的用for循环,不知道循环次数但知道循环结束条件的用while循环,do...while循环用的少
break 和 continue
-
break: 结束距离当前位置最近的一个结构
-
continue: 跳过当前循环,直接进入下一次循环,continue语句后面的语句不会再执行
数组
数组是引用类型,直接打印结果如下,[I@2dda6444 的 ‘[’ 和‘@’是固定形式,其中的‘I’表示的是该变量存储的数据类型是int类型,后面的‘2dda6444’是该数组在堆空间的地址值,该数组是以16进制形式展示的,我们可以看下数组继承顶级父类Object的toString方法
1. 数组的toString()方法
数组并没有对object的toString()方法进行重写,所以数组的toString()方法是直接继承的父类Object的
我们打印数组的结果是'[I@2dda6444' ,我们可以看见它的结果就是上面 某某某 + '@' + Integer.toHexString(hashCode())拼接而成的,Integer.toHexString() 方法是对传进来的数以16进制的结果返回
所以我们现在想要搞明白的是,这个'hashCode()'是什么方法,它返回的是什么,在Object类中找这个方法
可以看见它文档注释上说的是: 返回这个对象的哈希值,该方法主要是拿来支持后面的HashMap列表的,判断两对象是否是同一个对象的。该方法被'native'修改,标明该方法不是java代码直接实现的,而是依赖于系统底层库函数来实现的。
这个时候我们就应该去chargpt了
所以可以看见,是将该对象的哈希值以16进制拼接返回的,该哈希值并不是该对象的存储地址,只是与存储地址有关,是基于存储地址来经过特殊算法计算得到的,该哈希值分别更加均匀,便于提高了哈希表的效率。注意,即使不同的对象,哈希值也可能相同,这被称为哈希冲突,只是概率很小。
2. 数组的初始化,以及元素的访问
// 静态初始化 // 方式一: int[] a = new int[]{1,2,3,4,5}; // 方式二: 其中的new int[]被省略了,但实际该数组底层还是new出来的 int[] b = {1,2,3,4,5}; // 数组元素的访问 for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } // 动态初始化 int[] c = new int[10]; for (int i = 0; i < c.length; i++) { c[i] = i; }
注意事项
-
数组是引用数据类型,所以当一个对象的成员变量是数组时,不能浅拷贝给其它对象,否则两对象共用的是同一个数组。
-
动态初始化的数组,当未赋初始值时候,系统会默认赋值
1. int,long的默认赋值--0
2. float,double的默认赋值--0.0
3. char的默认赋值--''
4. String的默认赋值--null
方法
方法是程序中最小的执行单元,可以将重复编写的代码抽取到方法中,提高代码的复用性和可维护性
方法定义的格式
public后可以加修饰词,比如state final等
其中state代表的是该方法是静态方法,可以不用创建对象直接调用该方法,一般用 类名.方法名(参数) 调用
1. 方法的重载
方法的重载一般用来编写方法时,编写同样功能兼容返回值类型不同和形参不同的用的一种手段
2. 引用数据类型和基本数据类型
因为引用类型赋值给其它变量时,赋的是地址值,所以当引用类型作为实参传递给方法时,在方法中修改该形参,实际修改的是方法外的实参
3. 二维数组的初始化(静态和动态)
有一种特殊情况是:二维数组是这样初始化的
public class array { public static void main(String[] args) { int[][] a = new int[10][]; int[] b = new int[10]; int[] c = new int[5]; a[0] = b; a[1] = c; } }
面向对象(类)
在一个类中,它可以拥有成员变量,成员方法,构造器,代码块和内部类
1. 成员变量类型前的修饰符
该变量的访问权限,如果是public,那么表示在任何类内创建该类的实体对象,都能访问到该实体对象的这个成员变量;如果是private,则表示该变量只能在该类内直接使用,其它任何类内不能直接使用,只能调用该类提供的公共可访问的方法去访问该变量或者是子类继承下来的访问方法来访问。如果是protected,则在同一个包内或继承的子类内可以使用。
2. 就近原则和this关键字
在类的方法中,如果形参的名称与成员变量的名称一样,那么在方法中直接使用该变量,那么实际使用的是该方法的成员变量,如果想使用该类的成员变量,则需要使用 'this.'成员变量名 去访问,this关键词的在此的实际含义是:谁调用就代表谁的地址值,所以通过地址值去访问变量,就能访问到成员变量了
3. 构造方法
在每个类中都会默认提供一个无参构成方法(当你没有提供任何有参构造时),如果提供了有参构造则系统不再提供无参构造,你需要自己编写
构造方法一般是用来给成员变量初始化值的
如果你不想让别人直接创建该对象,则你可以把构造方法私有化,然后再提供创建对象的方法即可
字符串
String字符串,它是引用类型,其本质上和Integer等是一样的,都是封装类。
1. 字符串的值不可改变
字符串值是不可发生改变的,它们的值在创建后不能被更改,如果你把它的值进行了修改,实际上它并不是把它的值修改了,而是创建了另一个新的字符串对象,并把该字符串对象的地址值赋予了给原来的变量
2. 字符串对象创建的两种方法(直接赋值和new)
直接赋值创建字符串时,会先检查堆空间中的串池是否含有该字符串,如果有,则直接将该字符串的地址赋值给它进行复用,如果不存在才在串池中创建新的字符串,再将新的地址值赋值回去
而通过new创建字符串,那就是直接在堆空间中开辟一片新的空间存储字符串,然后再将该地址值赋值回去
3. 源码分析
-
String类的成员变量
可以看见String类拥有成员变量'byte[] value','coder','hash','hashIsZero',等。
-
其中的value字节数组是用来存储真实字符串的值的,可以看见它的修饰符为final,由此可见字符串的值是不可以改变的。如果将它的值改变,实际上是给字符串变量new了一个新的字符串对象。
-
coder成员变量,是用来存储该字符串的编码方式的,它的值有UTF16和LATIN1
-
hash成员变量是用来缓存该字符串的hashcode值的
可见String类重写了hashCode()方法,方法中判断hash是否在之前已经计算好了,如果是直接返回hash就行,如果没有,那么就是第一次计算,计算好了再返回,同时给hash赋值。
为什么要这么做?因为字符串是不可变的,所以我们每创建一个新的字符串都会在串池中存储好,后期可进行复用,然后字符串因为一般比较长,计算成本高,所以可以将其的值进行保存,提高效率,同时还便于了hash表的使用。
-
hashIsZero成员变量,因为hash值可能计算出来就是0,然后hash成员变量的默认值为0,所以为了将两者值区分开,从而引入的hashIsZero成员变量。
-
-
String.charAt() 方法
根据字符串的编码方式,调用不同的方法,返回字符的值
-
String.startsWith() 和 String.endWith()方法
-
重写hashCode方法
上几行已经写了就不再重复写了
-
重写equals 方法
先判断地址值是否相同,是否是同一个对象,如果是直接返回true,否则再比较字符串值
-
重写toString方法
父类Object的toString方法返回的就是String类型的对象,所以这直接对this该对象直接返回即可
有人会问,如果返回的是this字符串对象,众所周知,当我们使用'System.out.println()'的时候,用它打印字符串对象时,字符串对象本质上既然是个封装类,那为什么直接打印字符串对象,打印出来的结果就是value字节数组的值呢?
我在网上找了很多帖子,想查清这个问题,但没有一张帖子是解释了这个问题的,后面我只能求助chargpt,gpt解释说,是因为jvm虚拟机在运行代码时候,对String类做了特殊处理的,使它知道使用该类,打印出来是打印value字节数组的值。同样有类似处理的还有各种基本类型的封装类。这个回答对我来说还是能接收的,逻辑上至少是没什么问题的。
还有一个问题就是,object既然是String的父类,那么为什么它却可以在toString方法中返回的是子类的类型(String),后面我测试证明,在父类中是可以使用子类类型的
4.常用方法
-
equals 和 equalsIgnoreCase 字符串是否相等 字符串是否相等(忽略大小写)
-
charAt 获得字符串中下标为index的字符
-
compareTo 和 compareToIgnoreCase 字符串比较大小 字符串比较大小(忽略大小写)
-
contains 是否含有某字符串
-
getBytes 获得字节数组
-
isEmpty 字符串是否长度为0
-
indexOf 获取某段字符串在字符串中的下标,如果没有返回-1
-
length 获取字符串长度
-
replace 替换
-
split 切割
-
startsWith 以某字符串开头
-
endsWith 以某字符结尾
-
toLowerCase 和 toUpperCase 转大小写
-
valueOf 获取字符串
-
trim 去除首位空格
StringBuilder 和 StringBuffer
为什么会有 StringBuilder 和 StringBuffer,这是因为 String 类型的不可修改性和复用性,在大量字符串做修改时候,修改次数特别多,创建了很多无用的字符串同时,导致浪费空间效率低下,从而引出的问题
可以试一下下面的代码,你就知道这种情况下,String类型的效率是有多么的低
public static void main(String[] args) { String t = ""; for (int i = 0; i < 1000000; i++) { t += i; } System.out.println(t); }
你简单的看循环次数不过才1e6,但每次循环的时间却常的可怕
上面代码(字符串拼接的底层原理):
字符串拼接分为2种情况:
-
没有字符串变量参与的拼接: 那么在编译阶段就会之间被优化成一个拼接好的字符串
-
有字符串变量参与拼接: 又要分为两种情况
-
在jdk8以前,每次字符串拼接底层都是new了一个StringBuild,然后再调用append方法将字符串加进入,然后再调用StringBuild的toString方法,转成字符串对象。在该过程是是创建了一个StringBuild对象,然后又调用toString方法又创建了一个String对象,所以一个拼接操作,就创建了两个对象,并且这两个对象后期不会进行复用,导致效率低下。
-
在jdk8及以后,对字符串拼接操作底层做了优化,它是直接预估两个字符串最终拼接的长度,然后创建一个数组,将这两个字符串加进入,本质是创建了一个字符串对象。该对象后期也不能进行复用,所以也会造成效率的低下。
-
但下面的这个代码的效率缺强的可怕
public static void main(String[] args) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < 1000000; i++) { builder.append(i); } String t = builder.toString(); System.out.println(t); }
既然它效率这么强,那肯定是和它的底层原理实现有关的,我们去看一下它的源码
1. StringBuilder源码分析
可以看见它是继承了 'AbstractStringBuilder' 抽象类的,对里面的大部分方法进行了重写,但也没怎么重写,基本上都是调用了父类的方法,然后再将自己返回。我们分析一下它的无参构造和有参构造,已经常用的append方法
-
无参构造
调用了父类的该方法,并且将16作为参数传递过去
在父类的该方法中,应该是先判断了字符串的编码,这我们就不选UTF16编码了,选简单的来看
所以可以看见该父类直接给value赋值,赋了一个新的字节数组,大小为16,这的value是它的成员变量,和String的value字节数组是一样的,都是存储字符串真实值的。
这个就是无参构造
我们看一下'AbstractStringBuilder'的成员变量
-
'AbstractStringBuilder'的成员变量
可以看见真实存储字符串的值的还是字节数组value,但与String类的字节数组不同的是,该数组不是final,不再是不可修改的。
所以赋予一个新的值时,不再是新创建一个对象了,无论你将它的值改变多少次,它都只是一个对象,只需要将它的value数组改变即可。
-
有参构造
在有参构造器中,先将字符串传递给父类有参构造器方法,在父类有参构造器方法中,先判断该字符串的长度是否小于int的最大值-16,如果小于,则给value赋值初始大小是16 + str的长度,否则直接走int的最大值长度即可,然后再调用append方法,将字符串值它加进入。append方法就下一个分析中看。
-
append方法
可以看见,如果传递的参数是object的,那么直接将它转成String类型,再调用下面形参是String类型的append方法。
看一下父类的append方法
先判断形参str是不是空,如果是,则直接调用appendNull方法,直接加一个字符串“null”即可。
否则先获取str的长度,然后再调用ensureCapacityInternal方法进行动态扩容
我们先看一下ensureCapacityInternal方法
该方法的逻辑是先计算出旧容量oldLength,然后让传进来的最小容量形参去减去旧的容量,如果大于0,就是旧的容量不够,明显装不下,那么就需要扩容,调用newCapacity去扩容,再看下扩容方法
该方法中,先获得旧的长度,和新的长度,以及它们之间的差值,然后调用ArraysSupport.newLength(oldLength, growth, oldLength + (2 << coder));如果最小增长旧容量 + 2个容量可以满足要求,就只增长旧容量 + 2个容量,如果不够,就以最小容量增长为准。
再回到刚才的ensureCapacityInternal方法,将value = Arrays.copyOf(value,newCapacity(minimumCapacity) << coder);
就旧值拷贝过去
这样的话就将旧值复制过去了,同时数组大小也扩大了。
再回到append方法,再调用putStringAt(count, str)即可,同时count += len;
-
reverse方法
-
toString() 方法
注意:在AbstractStringBuilder类中,count != value.length(),因为coder不同,编码方式不同,所以存储方式不同。
String类内,也是同样的道理
2. StringBuilder常用方法
-
append 末尾插入
-
compareTo 字典序比较
-
delete 删除起始位置到末尾位置
-
deleteCharAt 删除指定位置
-
replace(int start, int end, String str) 指定范围替换
-
indexOf 查第一次出现的位置
-
insert 指定位置插入
-
lastIndexOf 查最后一次出现的位置
-
reverse 反转
-
toString 返回字符串
3. StringBuffer源码分析
可以看见StringBuffer也是继承'AbstractStringBuilder'类,StringBuffer对它方法重写,其实也只是在原来的方法前加上'synchronized'修饰符而已,从而限制了线程的访问。所以其实StringBuffer的所有方法都比StringBuild多了个synchronized,其它的都是一样的。StringBuffer的速度 > StringBuilder > String