学习自b站up主青空の霞光的教程
将idea新ui代替旧ui的步骤:
按两下Shift键出现搜索框,输入“registry”找到ide.experimental.ui,将其勾选上,重启IDEA就变成新的UI样式了。
注释:
单行注释:// 多行注释:/**/ /**来进行更加详细的文档注释:
final关键字定义的变量为常量,无法重复定义

为什么会出现以上的情况呢,注意跟整型常量赋值给浮点型变量一样,会隐式类型转换不同,由于float类型精度不如double,这个直接赋值是对float类型变量赋值了一个double类型数据,会不合法。
总结一下隐式类型转换规则:byte→short(char)→int→long→float→double
字符类型 char
字符类型也是一个重要的基本数据类型,它可以表示计算机中的任意一个字符(包括中文,英文, 标点符号等一切可以显示出来字符)表示的范围是0-65535
注意String类型:
这种类型并不是基本数据类型,它是对象类型
布尔类型
布尔类型是Java中的一个比较特殊的类型,它并不是存放数字的,而是状态,它有下面的两个状态:true 真 false 假。布尔类型(boolean)只有true和false两种值,也就是要么为真,要么为假,布尔类型的变量通常用作流程控制判断语句(不同于C语言,C语言中一般使用0表示false,除0以外的所有数都表示true)布尔类型占据的空间大小并未明确定义,而是根据不同的JVM会有不同的实现。

运算符
不同类型之间也可以进行运算:
注意:不同类型的整数一起运算,小类型需要转换为大类型,short、byte、char一律转换为int再进行计算(无论算式中有无int,都需要转换),结果也是int;如果算式中出现了long类型,那么全部都需要转换到long类型再进行计算,结果也是long,反正就是依大的来。

字符串拼接:
字符串不仅可以跟字符串拼接,也可以跟基本数据类型拼接:

然后可以用转义字符char a = '\n' 或者 '\"'表示特殊的字符
乘除方面需要注意的是
如果是两个int类型的值进行除法运算,正常情况下为1.6,但由于结果也是整数,所以最后小数部分被丢弃了。

如果是两个小数一起计算的话,因为结果也是小数,所以说就可以准确得到结果:

同样的,整数和小数一起计算,由于所有的整数范围都比小数小,根据转换规则,整数和小数一起计算时,所有的整数都会变成小数参与运算,所以说最后的结果也就是小数了,同样可以得到正确的结果
| 优先级 | 运算符 | 结合性(出现同优先级运算符时) |
|---|---|---|
| 1 | -(负号) +(正号) | 从右向左 左结合性 |
| 2 | * / % | 从左往右 右结合性 |
| 3 | +(加法,包括字符串) -(减法) | 从左往右 右结合性 |
| 4 | = | 从右向左 左结合性 |
括号运算符
加减法优先级比乘除法优先级低,所以引入了括号来提高算术优先级:


括号除了可以用来提升运算优先级,也可以用作强制类型转换,前面我们介绍了隐式类型转换,但是隐式类型转换存在局限性,比如此时我们希望将一个大的类型转换为一个小的类型:

强制类型转换存在一定的风险,比如:

解释:这里的128为 00000000 00000000 00000000 10000000 ->byte只有一个字节,所以说保留最后8位 :10000000 = -128
强制类型转换只有在明确不会出现问题的情况下,才可以使用。当然,强制类型转换也可以用在后面的类中。
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | ( ) | 从左向右 |
| 2 | - + (强制类型转换) | 从右向左 |
| 3 | * / % | 从左向右 |
| 4 | +(加法,包括字符串) -(减法) | 从左向右 |
| 5 | = | 从右向左 |
自增自减运算符:
自增自减是有两种形式的:b=++a或者b=a++,前者是先将a自增再赋值给b,后者是先将a赋值给b,然后a再自增。
自增自减运算符的优先级与正负号等价比如:

所以先计算左右两边的式子++是右结合性所以左边式子是-8,a现在为9,所以右边式子是10,总体为2。
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | ( ) | 从左向右 |
| 2 | - + (强制类型转换) ++ -- | 从右向左 |
| 3 | * / % | 从左向右 |
| 4 | +(加法,包括字符串) -(减法) | 从左向右 |
| 5 | = += -= *= /= %= | 从右向左 |
位运算
位运算偏向于底层,直接使用位运算符以二进制形式操作目标,位运算符包括:&、|、^、~
按位与&
9 & 3 = 1
实际上是 9:1001 3:0011
1001
& 0011
_____________
0001
同理,按位或,就是只要任意为1,那么结果就是1。
注意,按位异或符号初学者会以为是乘方运算,但是JAVA中并没有乘方符号,^是异或符号。
9 ^ 3 = 10 实际上就是:9:1001 3:0011
1001
^ 0011
________________
1010 == 2^1 + 2 ^ 3 = 10
按位取反操作与之前的正负号一样,只操作一个数,最好理解,如果这一位上是1,变成0,如果是0,变成1:


- 也就是127:0111 1111
- ~128 = 1000 0000 = -128
除了以上四个运算符之外,还有位移运算符,比如:
1 << 2 = 4 => 0001 >> 2 = 0100 = 4,尾部使用0填充,此时就是4。发现规律:左移操作每进行一次,结果就会X2,所以说除了直接使用*进行乘2的运算之外,我们也可以使用左移操作来完成。
同理:右移操作就是向右移动每一位。
8 >>2 = 2 => 1000 >> 2 = 0010 = 2,(头部使用符号位数字填充,此时是2),发现规律:右移操作可以快速进行除以2的计算。
对于负数来说,左移和右移操作不会改变其符号位上的数字,符号位不受位移操作影响。


- 也就是 -4 = 1111 1100
- 1111 1100 >> 2 = 1111 1111 =>取反+1=原码 = -1
所以总结一下:
- 左移操作<<:高位直接丢弃,低位补0
- 右移操作>>:低位直接丢弃,符号位是什么就补什么
以上是有符号(不动符号位)的位移操作,>>>是无符号(符号位需要变)的位移操作
最后我们还是来总结一下优先级:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | ( ) | 从左向右 |
| 2 | ~ - + (强制类型转换) ++ -- | 从右向左 |
| 3 | * / % | 从左向右 |
| 4 | + - | 从左向右 |
| 5 | << >> >>> | 从左向右 |
| 6 | & | 从左向右 |
| 7 | ^ | 从左向右 |
| 8 | | | 从左向右 |
| 9 | = += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左 |
关系运算符:>= <= > < ==
关系短路: a = 150, b = 100 >= a && a >= 60
上来就不满足条件,此时因为&&为两个都为真才为真。 第一个为假,不再往后判断,总体为假。
总结:&&与运算是如果第一个条件为假,就不会往后判断了,不会影响结果
||或运算是如果第一个条件为真真,就不会往后判断了,不会影响结果。
public static void main(String[] args) {
int a = 10;
boolean b = a++ > 10 && ++a == 12;
System.out.println("a = "+a + ", b = "+b);
}
解析:&&与运算就从左到右运算,有一个为假,就不会判断了,第一个条件a++>10,将a先与10相比,再让a自增1,a>10为假,则不会往后判断了。
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | ( ) | 从左向右 |
| 2 | ~ - + (强制类型转换) ++ -- | 从右向左 |
| 3 | * / % | 从左向右 |
| 4 | + - | 从左向右 |
| 5 | << >> >>> | 从左向右 |
| 6 | > < >= >= | 从左向右 |
| 7 | == != | 从左向右 |
| 8 | & | 从左向右 |
| 9 | ^ | 从左向右 |
| 10 | | | 从左向右 |
| 11 | && | 从左向右 |
| 12 | || | 从左向右 |
| 13 | ? : | 从右向左 |
| 14 | = += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左 |
代码块与作用域
变量的使用范围,仅限于其定义时所处的代码块,一旦超出对应的代码块区域,那么就相当于没有这个变量了。最近的大括号。
流程控制语句
当然switch中可以继续嵌套其他的流程控制语句,比如if:
public static void main(String[] args) {
char c = 'A';
switch (c) {
case 'A':
if(c == 'A') { //嵌套一个if语句
System.out.println("表扬!");
}
break;
case 'B':
System.out.println("去平行班!");
break;
}
}
for循环语句 for(表达式1:表达式2:表达式3)循环体
表达式1:在循环开始时仅执行一次
表达式2:每次循环开始前会执行一次,要求为判断语句。用于判断是否进入循环,若为真,那么进入循环,否则结束循环。
表达式3:每次循环完成执行一次。
循环体:每次循环都会执行一次循环体。
break为结束循环,continue为加速循环,跳过当前循环。
虽然使用break和continue关键字能够更方便的控制循环,但是注意在多重循环嵌套下,它只对离它最近的循环生效(就近原则):

如果一个代码块中存在多个循环,那么直接对当前代码块的标记执行break时会直接跳出整个代码块:

水仙花数
“水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant, PPDI)、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指**一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身。**例如:1^3 + 5^3+ 3^3 = 153。”


九九乘法表:


类与对象
对象是某一类事物实际存在的每个个体,因而也被称为实例(instance)我们每个人都是人类的一个实际存在的个体。

我们同样可以使用一个变量来指代某个对象,只不过引用类型的变量,存储的是对象的引用,而不是对象本身。类似于指针差不多。

方法定义时编写的参数叫,形式参数,而调用方法实际传入的参数,我们称为实际参数。
return 之后的语句不可到达。
方法的构造与使用

这是一个大坑:确实是值传递,但是前面说引用类型的变量仅仅存放的是对象的引用,而不是对象本身。那么这里的值传递,相当于将对象的引用复制到了方法内部的变量中,而这个内部的变量,依然是引用的同一个对象,所以说这里在方法内操作,相当于直接操作外面的定义对象。

方法的重载:
一个类中包含多个重名的方法,但是需要的形式参数不一样,方法的返回类型,可以相同,可以不同,但是仅返回类型不同,是不允许的!
特别:
方法之间可以相互调用。如果说互相方法调用,你调用我,我调用你,无休无止的调用,会产生栈溢出。

构造方法
构造方法不需要填写返回值,并且方法名称和类名相同,默认情况下每个类都会自带一个没有任何参数的无参构造方法(不去写,编译会自带的),可以对他进行修改。

构造方法在new的时候就是在调用无参的构造方法。
在创建构造方法的时候,我们可以创建带参的构造方法:

我们自己定义的构造方法之后,会覆盖掉默认的那一个无参构造方法。
去看反编译的结果,会发现此时没有无参构造了,而是我们自己编写的。


如果不给他赋值的话,就会使用默认的初始值。


我们可以在类中添加代码块,代码块同样在对象构造之前进行,在变量初始化之后执行。

执行顺序是:代码先初始化>>代码块>>构造方法
标准情况下还是通过构造方法进行进行对象初始化工作,所以说这里做了解就行了。
静态变量与静态方法:
成员变量:对象具有的属性,成员属性与成员方法与构造方法。
静态的内容:可以把它看做是属于这个类的,也可以理解为是所有对象共享的内容,我们可以使用关键字static关键字来声明一个变量或一个方法为静态的,那么通过这个类所创建的对象,操作的都是同一个目标,对象再多,一个对象改变了静态变量的值,那么其他对象读取的就是被改变的值。


所以说一般情况下,我们并不会通过一个具体的对象去修改和使用静态属性,而是通过这个类去使用:


静态方法同样是属于类的,而不是具体的某个对象,所以说,就像下面这样:

因为静态方法是属于类的,所以我们在静态方法中,无法获取成员变量,就好比说某个人的姓名是什么,没有说人类的姓名。

静态变量什么时候初始化呢?
实际上怎么执行?是将.class文件丢给jvm去执行,而每一个.class文件其实就是我们编写的一个类,我们在Java中使用一个类前jvm并不会在一开始就去加载它,而是在需要时才会去加载(优化)一般遇到以下情况时才会会加载类:
1.访问类的静态变量,或者为静态变量赋值
2.new创建类的实例(隐式加载)
3.调用类的静态方法
4.子类初始化时
5.反射介绍
所有被标记为静态的内容,会在类刚加载的时候就分配,而不是在对象创建的时候分配,所以说静态内容一定会在第一个对象初始化之前完成加载。

当然,如果我们压根就没有去使用这个类,那么也不会被初始化了。
包与访问控制
注意,在不同包下的类,即使类名相同,也是不同的两个类:
如:

由于默认导入了系统自带的String类,并且也导入了我们自己定义的String类,那么此时就出现了歧义,编译器也不知道我们想要用哪一个。
实际上,java就像我们的个人隐私一样,有自己的访问权限。
- private 私有 标记为私有的内容无法被除了当前类以外的任何位置访问。
- 什么都不写 默认,默认情况下,只能被类本身和同包中的其他类访问。
- protected 受保护,标记为受保护的内容可以能被类本身和同包中的其他类访问,也可以被子类访问(子类我们会在下一章介绍)
- public 公共,标记为公共的内容,允许在任何地方被访问。
-
这四种访问权限,总结如下表:
注意,我们创建的普通类不能是protected或是private权限,因为如果当前普通类是给当前的包内使用或者是给外面使用,如果用的是private,谁都不能用,为什么要创建呢。当前类 同一个包下的类 不同包下的子类 不同包下的类 public ✅ ✅ ✅ ✅ protected ✅ ✅ ✅ ❌ 默认 ✅ ✅ ❌ ❌ private ✅ ❌ ❌ ❌
封装
封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装
也就是说,外部现在只能通过调用我定义的方法来获取成员属性。
如果不允许外部使用new创建对象。

使用单例模式创建对象。

另一种方式:

封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成,封装就是通过访问权限控制来实现的。
继承
在定义不同类的时候存在一些相同属性,为了方便可以使用这些共同属性抽象成一个父类,在定义其他子类时可以继承父类,减少代码重复性,子类可以使用父类非私有的成员。
实际上划分出来的类,本质上还是人类,也就是说人类具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类也会拥有自己独特的技能。

是不是感觉非常人性化,子类继承了父类的全部能力,同时还可以扩展自己的独特能力,就像一句话说的: 龙生龙凤生凤,老鼠儿子会打洞。
如果父类存在一个有参构造方法,子类必须在构造方法中调用:

强制类型转换
向下转型:

向上转型:


instanceof判断变量是否是所引用的对象。它的作用是判断其左边对象是否为其右边类的实例

如果父类的name属性和子类的name属性是同时存在的,怎么去访问父类的呢?同样可以使用super关键字来表示父类。


但是注意,没有super.super这种用法,也就是说如果存在多级继承的话,那么最多只能通过这种方法访问到父类的属性(包括继承下来的属性)。
顶层Object类
所有的类都默认继承自Object类,除非手动指定继承的类型,但是依然改变不了顶层的父类是Object类。

public class Object {
private static native void registerNatives(); //标记为native的方法是本地方法,底层是由C++实现的
static {
registerNatives(); //这个类在初始化时会对类中其他本地方法进行注册,本地方法不是我们SE中需要学习的内容,我们会在JVM篇视频教程中进行介绍
}
//获取当前的类型Class对象,这个我们会在最后一章的反射中进行讲解,目前暂时不会用到
public final native Class<?> getClass();
//获取对象的哈希值,我们会在第五章集合类中使用到,目前各位小伙伴就暂时理解为会返回对象存放的内存地址
public native int hashCode();
//判断当前对象和给定对象是否相等,默认实现是直接用等号判断,也就是直接判断是否为同一个对象
public boolean equals(Object obj) {
return (this == obj);
}
//克隆当前对象,可以将复制一个完全一样的对象出来,包括对象的各个属性
protected native Object clone() throws CloneNotSupportedException;
//将当前对象转换为String的形式,默认情况下格式为 完整类名@十六进制哈希值
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
//唤醒一个等待当前对象锁的线程,有关锁的内容,我们会在第六章多线程部分中讲解,目前暂时不会用到
public final native void notify();
//唤醒所有等待当前对象锁的线程,同上
public final native void notifyAll();
//使得持有当前对象锁的线程进入等待状态,同上
public final native void wait(long timeout) throws InterruptedException;
//同上
public final void wait(long timeout, int nanos) throws InterruptedException {
...
}
//同上
public final void wait() throws InterruptedException {
...
}
//当对象被判定为已经不再使用的“垃圾”时,在回收之前,会由JVM来调用一次此方法进行资源释放之类的操作,这同样不是SE中需要学习的内容,这个方法我们会在JVM篇视频教程中详细介绍,目前暂时不会用到
protected void finalize() throws Throwable { }
}

方法的重写

重写Object的equals方法,就会按照我们的方式进行判断了。

为了方便查看对象的各个属性,我们可以将Object类提供的toString方法重写了:

静态方法不支持重写,因为它属于类本身,但是它可以被继承
基于这种方法可以重写的特性,对于一个类定义的行为,不同的子类可以出现不同的行为,比如考试,学生考试可以得到A,而工人去考试只能得到D:

如果我们不不希望子类重写某个方法,我们可以在方法前添加final关键字,表示这个方法已经是最终形态了。
或者,如果父类中方法的可见性为private,那么子类同样无法访问,也就不能重写,但是可以定义同名方法,虽然可以编译通过,但这并不是对父类的重写,而仅仅是子类自己创建的一个方法。
访问权限问题,子类在重写父类方法时候,不能降低父类方法中的可见性。

可以看到作为子类时就可以正常调用,但是如果将其作为父类使用,因为访问权限不足所有就无法使用,总之,子类重写的方法权限不能比父类还低。
抽象类
越是处于顶层定义的类,实际上可以进一步地进行抽象,比如我们前面编写的考试方法:
比如:人考试是一个抽象概念,而学生和工人怎么考试,才是一个具体的实现,父类中不需要提供实现。
把人类变成抽象类,抽象类比类还要抽象:
我们把人变成抽象类,方法exam也变成抽象方法,具体实现在它继承的的子类中具体实现。



接口
1.接口包含了一些类方法的定义,类可以实现这个接口。(类似于一个插件,只能作为一个附加属性加载到主体上,具体实现还需要由主体来实现),对于人类说,学生和老师他们都具有学习这个能力,那么可以将学习这个能力抽象成接口来进行使用,只要实现这个接口的类,都要实现学习这个方法。
认为:有人说接口是Java的多继承,但我个人认为错的,实际上实现接口更像是一个类的功能列表出现,作为附加功能存在。
接口跟抽象类一样,不能直接创建对象,但是我们也可以将接口实现类的对象以接口的形式去使用:

接口同样支持向下转型:

如果方法在接口中存在默认实现,那么实现类中不强制要求进行实现。

接口不同于类,接口中不允许存在成员变量和成员方法,但是可以存在静态变量和静态方法,在接口中定义的变量只能是:
public interface Study {
public static final int a = 10; //接口中定义的静态变量只能是public static final的
public static void test(){ //接口中定义的静态方法也只能是public的
System.out.println("我是静态方法");
}
void study();
}
跟普通的类一样,我们可以直接通过接口名.的方式使用静态内容:
public static void main(String[] args) {
System.out.println(Study.a);
Study.test();
}
最后我们来介绍一下Object类中提供的克隆方法,为啥要留到这里才来讲呢?因为它需要实现接口才可以使用:
子类继承父类,需要实现Cloneable,需要重写clone方法




类重写的方法的优先级大于接口。
枚举类
//这里使用javap命令对class文件进行反编译得到 Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum<com.test.Status> {
public static final com.test.Status RUNNING;
public static final com.test.Status STUDY;
public static final com.test.Status SLEEP;
public static com.test.Status[] values();
public static com.test.Status valueOf(java.lang.String);
static {};
}
包装类

装箱


拆箱


因为a和b是两个不同的引用,所以其地址是不同的,所以是false
如果超过127的范围,就会得到不同的对象了。

可以对8,16进制数转换为10进制数。

10进制转换为8或16进制

特殊包装类BigInteger
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE); //表示Long的最大值,轻轻松松
System.out.println(i);
}
我们可以通过调用类中的方法,进行运算操作:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE);
i = i.multiply(BigInteger.valueOf(Long.MAX_VALUE)); //即使是long的最大值乘以long的最大值,也能给你算出来
System.out.println(i);
}
我们来看看结果:

可以看到,此时数值已经非常大了,也可以轻松计算出来。咱们来点更刺激的:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE);
i = i.pow(100); //long的最大值来个100次方吧
System.out.println(i);
}
可以看到,这个数字已经大到一排显示不下了:

一般情况,对于非常大的整数计算,我们就可以使用BigInteger来完成。
我们接着来看第二种,前面我们说了,浮点类型精度有限,对于需要精确计算的场景,就没办法了,而BigDecimal可以实现小数的精确计算。
public static void main(String[] args) {
BigDecimal i = BigDecimal.valueOf(10);
i = i.divide(BigDecimal.valueOf(3), 100, RoundingMode.CEILING);
//计算10/3的结果,精确到小数点后100位
//RoundingMode是舍入模式,就是精确到最后一位时,该怎么处理,这里CEILING表示向上取整
System.out.println(i);
}
可以看到,确实可以精确到这种程度:

但是注意,对于这种结果没有终点的,无限循环的小数,我们必须要限制长度,否则会出现异常。
一维数组
数组是相同类型数据的有序集合,数组可以代表任何相同类型的一组内容(包括引用类型和基本类型)其中存放的每一个数据称为数组的一个元素,我们来看看如何去定义一个数组变量:
public static void main(String[] args) {
int[] array; //类型[]就表示这个是一个数组类型
}
注意,数组类型比较特殊,它本身也是类,但是编程不可见(底层C++写的,在运行时动态创建)即使是基本类型的数组,也是以对象的形式存在的,并不是基本数据类型。所以,我们要创建一个数组,同样需要使用new关键字:
public static void main(String[] args) {
int[] array = new int[10]; //在创建数组时,需要指定数组长度,也就是可以容纳多个int变量的值
Object obj = array; //因为同样是类,肯定是继承自Object的,所以说可以直接向上转型
}
除了上面这种方式之外,我们也可以使用其他方式:
类型[] 变量名称 = new 类型[数组大小];
类型 变量名称[] = new 类型[数组大小]; //支持C语言样式,但不推荐!
类型[] 变量名称 = new 类型[]{...}; //静态初始化(直接指定值和大小)
类型[] 变量名称 = {...}; //同上,但是只能在定义时赋值
创建出来的数组每个位置上都有默认值,如果是引用类型,就是null,如果是基本数据类型,就是0,或者是false,跟对象成员变量的默认值是一样的,要访问数组的某一个元素,我们可以:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println("数组的第一个元素为:"+array[0]); //使用 变量名[下标] 的方式访问
}
注意,数组的下标是从0开始的,不是从1开始的,所以说第一个元素的下标就是0,我们要访问第一个元素,那么直接输入0就行了,但是注意千万别写成负数或是超出范围了,否则会出现异常。
我们也可以使用这种方式为数组的元素赋值:
public static void main(String[] args) {
int[] array = new int[10];
array[0] = 888; //就像使用变量一样,是可以放在赋值运算符左边的,我们可以直接给对应下标位置的元素赋值
System.out.println("数组的第一个元素为:"+array[0]);
}
因为数组本身也是一个对象,数组对象也是具有属性的,比如长度:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println("当前数组长度为:"+array.length); //length属性是int类型的值,表示当前数组长度,长度是在一开始创建数组的时候就确定好的
}
注意,这个length是在一开始就确定的,而且是final类型的,不允许进行修改,也就是说数组的长度一旦确定,不能随便进行修改,如果需要使用更大的数组,只能重新创建。
当然,既然是类型,那么肯定也是继承自Object类的:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println(array.toString());
System.out.println(array.equals(array));
}
但是,很遗憾,除了clone()之外,这些方法并没有被重写,也就是说依然是采用的Object中的默认实现:

所以说通过toString()打印出来的结果,好丑,只不过我们可以发现,数组类型的类名很奇怪,是[开头的。
因此,如果我们要打印整个数组中所有的元素,得一个一个访问:
public static void main(String[] args) {
int[] array = new int[10];
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
有时候为了方便,我们可以使用简化版的for语句foreach语法来遍历数组中的每一个元素:
public static void main(String[] args) {
int[] array = new int[10];
for (int i : array) { //int i就是每一个数组中的元素,array就是我们要遍历的数组
System.out.print(i+" "); //每一轮循环,i都会更新成数组中下一个元素
}
}
是不是感觉这种写法更加简洁?只不过这仅仅是语法糖而已,编译之后依然是跟上面一样老老实实在遍历的:
public static void main(String[] args) { //反编译的结果
int[] array = new int[10];
int[] var2 = array;
int var3 = array.length;
for(int var4 = 0; var4 < var3; ++var4) {
int i = var2[var4];
System.out.print(i + " ");
}
}
对于这种普通的数组,其实使用还是挺简单的。这里需要特别说一下,对于基本类型的数组来说,是不支持自动装箱和拆箱的:
public static void main(String[] args) {
int[] arr = new int[10];
Integer[] test = arr;
}
还有,由于基本数据类型和引用类型不同,所以说int类型的数组时不能被Object类型的数组变量接收的:

但是如果是引用类型的话,是可以的:
public static void main(String[] args) {
String[] arr = new String[10];
Object[] array = arr; //数组同样支持向上转型
}
public static void main(String[] args) {
Object[] arr = new Object[10];
String[] array = (String[]) arr; //不支持
//要这样
Object[] arr = new String[10];
String[] array = (String[]) arr; //不支持
}
多维数组
存放数组的数组,相当于将维度进行了提升,比如上面的就是一个2x10的数组:


可变长参数
我们接着来看数组的延伸应用,实际上我们的方法是支持可变长参数的,什么是可变长参数?
可变长数组是指,可以传入0-N个对应类型的实参。


注意,如果同时存在其他参数,那么可变长参数只能放在最后而且只能有一个:
字符串:
创建字符串的两个方式

public static void main(String[] args) {
String str1 = "Hello World";
String str2 = "Hello World";
System.out.println(str1 == str2);
}
true
public static void main(String[] args) {
String str1 = new String("Hello World");
String str2 = new String("Hello World");
System.out.println(str1 == str2);
}
false
因此,如果我们仅仅要判断两个字符串的内容是否相等不要用==,String类重载了equals方法用于判断内容相同。

字符数组和字符串之间是可以快速进行相互转换的:
public static void main(String[] args) {
String str = "Hello World";
char[] chars = str.toCharArray();
System.out.println(chars);
}
其他用法 大小写方法 str.lowerCase toUperCase startsWith indexOf
StringBuilder类
StringBuilder builder = new StringBuilder(); //一开始创建时,内部什么都没有
builder.append("AAA"); //我们可以使用append方法来讲字符串拼接到后面
builder.append("BBB");
System.out.println(builder); //当我们字符串编辑完成之后,就可以使用toString转换为字符串了
builder.replace替换某个字符 builder.reverse()反序

链式调用
正则表达式:

用于规定给定组件必须要出现多少次才能满足匹配的,我们一般称为限定符,限定符表如下:
| 字符 | 描述 |
|---|---|
| * | 匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。***** 等价于 {0,}。 |
| + | 匹配前面的子表达式一次或多次。例如,zo+ 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。 |
| ? | 匹配前面的子表达式零次或一次。例如,do(es)? 可以匹配 "do" 、 "does"、 "doxy" 中的 "do" 。? 等价于 {0,1}。 |
| {n} | n 是一个非负整数。匹配确定的 n 次。例如,o{2} 不能匹配 "Bob" 中的 o,但是能匹配 "food" 中的两个 o。 |
| {n,} | n 是一个非负整数。至少匹配n 次。例如,o{2,} 不能匹配 "Bob" 中的 o,但能匹配 "foooood" 中的所有 o。o{1,} 等价于 o+。o{0,} 则等价于 o*。 |
| {n,m} | m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,o{1,3} 将匹配 "fooooood" 中的前三个 o。o{0,1} 等价于 o?。请注意在逗号和两个数之间不能有空格。 |
如果我们想要表示一个范围内的字符,可以使用方括号:
public static void main(String[] args) {
String str = "abcabccaa";
System.out.println(str.matches("[abc]*")); //表示abc这几个字符可以出现 0 - N 次
}
对于普通字符来说,我们可以下面的方式实现多种字符匹配:
| 字符 | 描述 |
|---|---|
| [ABC] | 匹配 [...] 中的所有字符,例如 [aeiou] 匹配字符串 "google runoob taobao" 中所有的 e o u a 字母。 |
| [^ABC] | 匹配除了 [...] 中字符的所有字符,例如 [^aeiou] 匹配字符串 "google runoob taobao" 中除了 e o u a 字母的所有字母。 |
| [A-Z] | [A-Z] 表示一个区间,匹配所有大写字母,[a-z] 表示所有小写字母。 |
| . | 匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^\n\r] |
| [\s\S] | 匹配所有。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行。 |
| \w | 匹配字母、数字、下划线。等价于 [A-Za-z0-9_] |
正则表达式 Yes
成员内部类
这样子定义:

使用成员内部类

这里我们需要特别注意一下,在成员内部类中,是可以访问到外层的变量的:
public class Test {
private final String name;
public Test(String name){
this.name = name;
}
public class Inner {
public void test(){
System.out.println("我是成员内部类:"+name);
//成员内部类可以访问到外部的成员变量
//因为成员内部类本身就是某个对象所有的,每个对象都有这样的一个类定义,这里的name是其所依附对象的
}
}
}
public static void main(String[] args) {
Test a = new Test("小明");
Test.Inner inner1 = a.new Inner(); //依附于a创建的对象,那么就是a的
inner1.test();
Test b = new Test("小红");
Test.Inner inner2 = b.new Inner(); //依附于b创建的对象,那么就是b的
inner2.test();
}
那么如果内部类中也定义了同名的变量,此时我们怎么去明确要使用的是哪一个呢?
public class Test {
private final String name;
public Test(String name){
this.name = name;
}
public class Inner {
String name;
public void test(String name){
System.out.println("方法参数的name = "+name); //依然是就近原则,最近的是参数,那就是参数了
System.out.println("成员内部类的name = "+this.name); //在内部类中使用this关键字,只能表示内部类对象
System.out.println("成员内部类的name = "+Test.this.name);
//如果需要指定为外部的对象,那么需要在前面添加外部类型名称
}
}
}
包括对方法的调用和super关键字的使用,也是一样的:
public class Inner { String name; public void test(String name){ this.toString(); //内部类自己的toString方法 super.toString(); //内部类父类的toString方法 Test.this.toString(); //外部类的toSrting方法 Test.super.toString(); //外部类父类的toString方法 } }
静态内部类


局部内部类;
局部内部类就像局部变量一样,可以在方法中定义。
public class Test {
public void hello(){
class Inner{ //局部内部类跟局部变量一样,先声明后使用
public void test(){
System.out.println("我是局部内部类");
}
}
Inner inner = new Inner(); //局部内部类直接使用类名就行
inner.test();
}
}
匿名内部类:

普通类生成匿名内部类的方法:


当然,并不是说只有抽象类和接口才可以像这样创建匿名内部类,普通的类也可以,只不过意义不大,一般情况下只是为了进行一些额外的初始化工作而已。
Lambda表达式

如果一个接口中有且只有一个待实现的抽象方法,那么我们可以将匿名内部类简写为Lambda表达式。

如果有一个参数和返回值的话:

如果方法体中只有一个返回语句,可以直接省略花括号和return关键字,简洁版本。

Lambda使用变量要有final作为前缀,变量要一成不变的

方法引用
方法引用就是将一个已实现的方法,直接作为接口中抽象方法的实现(当然前提是方法定义得一样才行)
我们发现,Integer.sum的参数和返回值,跟我们在Study中定义的完全一样,所以说我们可以直接使用方法引用:
public static void main(String[] args) {
Study study = Integer::sum; //使用双冒号来进行方法引用,静态方法使用 类名::方法名 的形式
System.out.println(study.sum(10, 20));
}
方法引用其实本质上就相当于将其他方法的实现,直接作为接口中抽象方法的实现。任何方法都可以通过方法引用作为实现:
public interface Study {
String study();
}
如果是普通从成员方法,我们同样需要使用对象来进行方法引用:
public static void main(String[] args) {
Main main = new Main();
Study study = main::lbwnb; //成员方法因为需要具体对象使用,所以说只能使用 对象::方法名 的形式
}
public String lbwnb(){
return "卡布奇诺今犹在,不见当年倒茶人。";
}
因为现在只需要一个String类型的返回值,由于String的构造方法在创建对象时也会得到一个String类型的结果,所以说:
public static void main(String[] args) {
Study study = String::new; //没错,构造方法也可以被引用,使用new表示
}
反正只要是符合接口中方法的定义的,都可以直接进行方法引用,对于Lambda表达式和方法引用,在Java新特性介绍篇视频教程中还有详细的讲解,这里就不多说了。
异常机制
运算异常

运行时异常
编译时异常

错误:
public static void main(String[] args) {
test();
}
private static void test(){
test();
}
抛出异常
自定义异常
注意,如果我们在方法中抛出了一个非运行时异常,那么必须告知函数的调用方我们会抛出某个异常,函数调用方必须要对抛出的这个异常进行对应的处理才可以:

不同的异常抛出都需要注明:
private static void test(int a) throws FileNotFoundException, ClassNotFoundException { //多个异常使用逗号隔开
if(a == 1)
throw new FileNotFoundException();
else
throw new ClassNotFoundException();
}
异常的处理:
出现异常的时候我们希望能够自己处理出现的问题,让程序运行下去,需要对异常捕获:

使用try-catch,只要是这个范围内发生的异常,都可以被捕获,这里捕获的是NullPointerException空指针异常。
注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。

如果某个方法明确指出会抛出哪些异常,除非抛出的异常是一个运行时异常,否则我们必须要使用try-catch语句块进行异常捕获,不然无法通过编译。

数组越界异常是运行时异常的子类,所以可以捕获到。
代码可能出现多种类型的异常时,希望分不同情况处理不同的异常,可以使用多重异常捕获。

断言表达式:
我们可以使用断言表达式来对某些东西进行判断,如果判断失败会抛出错误。
数学工具类,与数组工具类。
实战训练,冒泡排序

二分查找

青蛙跳台阶问题
递归程序设计


回文串判断
汉诺塔



4015

被折叠的 条评论
为什么被折叠?



