final关键字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y4clTw5g-1649075023636)(https://cdn.nlark.com/yuque/0/2022/png/2699848/1647265729124-ebd1307f-a001-4079-abd9-8b6aec1c0979.png)]
抽象类
接口
接口的基础语法
接口在开发中的作用
类型和类型之间的关系
抽象类和接口的区别
package和import机制
package
import
java.lang下的类不需要,但是java.lang中的子包还是需要导的。
访问控制权限
1、访问控制权限都有哪些?
有4个
private 私有
public 公开
protected 受保护
默认
2、以上的4个访问控制权限,控制的范围是什么?
private 表示私有的,只能在本类中访问。
public 表示公开的,在任何位置都可以访问。
“默认” 表示只能在本类,以及同包下访问。
protected 表示只能在本类、同包、子类中访问。
访问控制修饰符 | 本类 | 同包 | 子类 | 任意位置 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
默认 | √ | √ | × | × |
private | √ | × | × | × |
范围从大到小排序:public > protected > 默认 > private
3、访问控制权限修饰符可以修饰什么?
属性(4个都能用)
方法(4个都能用)
类(public和默认能用,其他不行)
接口(public和默认能用,其他不行)
…
JDK类库的根类Object
这个老祖宗类中的方法我们需要研究一下,因为这些方法都是所有子类通用的。
任何一个类默认继承Object。就算没有直接继承,最终也会间接继承。
Object类当中有哪些常用的方法?
我们去哪里找这些方法呢?
第一种方法:去源代码当中。(但是这种方式比较麻烦,源代码也比较难)
第二种方法:去查阅java的类库的帮助文档。
什么是API?
应用程序编程接口。(Application Program Interface)
整个JDK的类库就是一个javase的API。
每一个API都会配置一套API帮助文档。
SUN公司提前写好的这套类库就是API。(一般每一份API都对应一份API帮助文档。)
需要知道几个方法:
protected Object clone() //负责对象克隆的。
int hashcode() //获取一个对象的哈希代码值
boolean equals(Object obj) //判断两个对象是否相等
String toString() //将对象转换成字符串形式
protected void finalize() //垃圾回收器负责调用的方法
toString()方法
1、源代码:
public String toString() {
return getClass().getName() + “@” + Integer.toHexString(hashCode());
}
源代码上toString()方法的默认实现是:
类名@对象的内存地址转换为十六进制的形式
2、SUN公司设计toString()方法的目的是什么?
toString()方法的设计目的是:通过调用这个方法可以将一个“java对象”转换成“字符串表示形式”
3、其实SUN公司开发java语言的时候,建议所有的子类都去重写toString()方法。
toString()方法应该是一个简洁的、详实的、易阅读的。
注:以后所有类的toString()方法是需要重写的。
System.out.println(引用);
这里会自动调用“引用”的toString()方法。
String类是SUN写的,toString方法已经重写了。
equals()方法
idea重写:
finalize()方法
hasecode()方法
内部类
例:
class Test01{
// 静态变量
static String country;
// 该类在类的内部,所以称为内部类
// 由于前面有static,所以称为“静态内部类”
static class Inner1{
}
// 实例变量
int age;
// 该类在类的内部,所以称为内部类
// 没有static叫做实例内部类。
class Inner2{
}
// 方法
public void doSome(){
// 局部变量
int i = 100;
// 该类在类的内部,所以称为内部类
// 局部内部类。
class Inner3{
}
}
public void doOther(){
// doSome()方法中的局部内部类Inner3,在doOther()中不能用。
}
// main方法,入口
public static void main(String[] args){
// 调用MyMath中的mySum方法。
MyMath mm = new MyMath();
/*
Compute c = new ComputeImpl();
mm.mySum(c, 100, 200);
*/
//合并(这样写代码,表示这个类名是有的。类名是:ComputeImpl)
//mm.mySum(new ComputeImpl(), 100, 200);
// 使用匿名内部类,表示这个ComputeImpl这个类没名字了。
// 这里表面看上去好像是接口可以直接new了,实际上并不是接口可以new了。
// 后面的{} 代表了对接口的实现。
// 不建议使用匿名内部类,为什么?
// 因为一个类没有名字,没有办法重复使用。另外代码太乱,可读性太差。
mm.mySum(new Compute(){
public int sum(int a, int b){
return a + b;
}
}, 200, 300);
}
}
// 负责计算的接口
interface Compute{
// 抽象方法
int sum(int a, int b);
}
// 你自动会在这里编写一个Compute接口的实现类
/*
class ComputeImpl implements Compute{
// 对方法的实现
public int sum(int a, int b){
return a + b;
}
}
*/
// 数学类
class MyMath{
// 数学求和方法
public void mySum(Compute c, int x, int y){
int retValue = c.sum(x, y);
System.out.println(x + "+" + y + "=" + retValue);
}
}
实例内部类
- 创建实例内部类,外部类的实例必须已经创建
- 实例内部类会持有外部类的引用
- 实例内部不能定义static成员,只能定义实例成员
静态内部类
- 静态内部类不会持有外部的类的引用,创建时可以不用创建外部类
- 静态内部类可以访问外部的静态变量,如果访问外部类的成员变量必须通过外部类的实例访问
局部内部类
局部内部类是在方法中定义的,它只能在当前方法中使用。和局部变量的作用一样。
局部内部类和实例内部类一致,不能包含静态成员。
数组
1、Java语言中的数组是一种**引用数据类型**。不属于基本数据类型。数组的父类是Object。
2、数组实际上是一个容器,可以同时容纳多个元素。(数组是一个数据的集合。)
数组:字面意思是“一组数据”
3、数组当中可以存储“基本数据类型”的数据,也可以存储“引用数据类型”的数据。
4、数组因为是引用类型,所以数组对象是堆内存当中。(数组是存储在堆当中的)
5、数组当中如果存储的是“java对象”的话,实际上存储的是对象的“引用(内存地址)”,数组中不能直接存储java对象。
6、数组一旦创建,在java中规定,**长度不可变**。(数组长度不可变)
7、数组的分类:一维数组、二维数组、三维数组、多维数组...(一维数组较多,二维数组偶尔使用!)
8、所有的数组对象都有length属性(java自带的),用来获取数组中元素的个数。
9、java中的数组要求数组中元素的类型统一。比如int类型数组只能存储int类型,Person类型数组只能存储Person类型。
例如:超市购物,购物袋中只能装苹果,不能同时装苹果和橘子。(数组中存储的元素类型统一)
10、数组在内存方面存储的时候,数组中的元素内存地址(存储的每一个元素都是有规则的挨着排列的)是**连续**的。**内存地址连续**。
这是数组存储元素的特点(特色)。数组实际上是一种简单的数据结构。
11、所有的数组都是拿“第一个小方框的内存地址”作为整个数组对象的内存地址。
(数组中**首元素的内存地址作为整个数组对象的内存地址**。)
12、数组中每一个元素都是有下标的,下标从0开始,以1递增。最后一个元素的下标是:length - 1
下标非常重要,因为我们对数组中元素进行“存取”的时候,都需要通过下标来进行。
数组这种数据结构的优缺点
13、数组这种数据结构的优点和缺点是什么?
**优点**:查询/查找/检索某个下标上的元素时效率极高。可以说是**查询效率最高的一个数据结构**。
为什么检索效率高?
第一:每一个元素的内存地址在空间存储上是连续的。
第二:每一个元素类型相同,所以占用空间大小一样。
第三:知道第一个元素内存地址,知道每一个元素占用空间的大小,又知道下标,所以
通过一个数学表达式就可以计算出某个下标上元素的内存地址。直接通过内存地址定位
元素,所以数组的检索效率是最高的。
数组中存储100个元素,或者存储100万个元素,在元素查询/检索方面,效率是相同的,
因为数组中元素查找的时候不会一个一个找,是通过数学表达式计算出来的。(算出一个
内存地址,直接定位的。)
**缺点**:
第一:由于为了保证数组中每个元素的内存地址连续,所以在数组上随机删除或者增加元素的时候,
效率较低,因为随机增删元素会涉及到后面元素统一向前或者向后位移的操作。
第二:数组**不能存储大数据量**,为什么?
因为很难在内存空间上找到一块特别大的连续的内存空间。
注意:对于数组中最后一个元素的增删,是没有效率影响的。
声明及初始化数组
怎么声明/定义一个一维数组?
1.数组元素的类型[] 变量名称
2.数组元素的类型 变量名称[]
//这是C++风格,不建议java中使用。
语法格式:
int[] array1;
double[] array2;
boolean[] array3;
String[] array4;
Object[] array5;
在一行中也可以声明多个数组,例如:int[] a,b,c
怎么初始化一个一维数组呢?
包括两种方式:静态初始化一维数组,动态初始化一维数组。
静态初始化语法格式:
int[] array = {100, 2100, 300, 55};
动态初始化语法格式:
int[] array = new int[5]; // 这里的5表示数组的元素个数。
// 初始化一个5个长度的int类型数组,每个元素默认值0
String[] names = new String[6]; // 初始化6个长度的String类型数组,每个元素默认值null。
访问数组中元素及遍历数组
public class ArrayTest01 {
public static void main(String[] args) {
// 声明一个int类型的数组,使用静态初始化的方式
int[] a = {1, 100, 10, 20, 55, 689};
// 这是C++风格,不建议java中使用。
//int a[] = {1, 100, 10, 20, 55, 689};
// 所有的数组对象都有length属性
System.out.println("数组中元素的个数" + a.length);
// 数组中每一个元素都有下标
// 通过下标对数组中的元素进行存和取。
// 取(读)
System.out.println("第一个元素 = " + a[0]);
System.out.println("最后一个元素 = " + a[5]);
System.out.println("最后一个元素 = " + a[a.length - 1]);
// 存(改)
// 把第一个元素修改为111
a[0] = 111;
// 把最后一个元素修改为0
a[a.length - 1] = 0;
System.out.println("第一个元素 = " + a[0]);
System.out.println("最后一个元素 = " + a[5]);
// 一维数组怎么遍历呢?
for(int i = 0; i < a.length; i++){
System.out.println(a[i]); // i是从0到5,是下标
}
// 下标为6表示第7个元素,第7个元素没有,下标越界了。会出现什么异常呢?
//System.out.println(a[6]); //ArrayIndexOutOfBoundsException(比较著名的异常。)
// 从最后一个元素遍历到第1个元素
for (int i = a.length - 1; i >= 0; i--) {
System.out.println("颠倒顺序输出-->" + a[i]);
}
}
}
动态初始化一维数组
什么时候采用静态初始化方式,什么时候使用动态初始化方式呢?
当你创建数组的时候,确定数组中存储哪些具体的元素时,采用静态初始化方式。
当你创建数组的时候,不确定将来数组中存储哪些数据,可以采用动态初始化的方式,预先分配内存空间。
public class ArrayTest02 {
public static void main(String[] args) {
// 声明/定义一个数组,采用动态初始化的方式创建
int[] a = new int[4]; // 创建长度为4的int数组,数组中每个元素的默认值是0
// 遍历数组
for (int i = 0; i < a.length; i++) {
System.out.println("数组中下标为" + i + "的元素是:" + a[i]);
}
// 后期赋值
a[0] = 1;
a[1] = 100;
a[2] = 111;
a[3] = 222; // 注意下标别越界。
for (int i = 0; i < a.length; i++) {
System.out.println("数组中下标为" + i + "的元素是:" + a[i]);
}
// 初始化一个Object类型的数组,采用动态初始化方式
Object[] objs = new Object[3]; // 3个长度,动态初始化,所以每个元素默认值是null
for (int i = 0; i < objs.length; i++) {
System.out.println(objs[i]);
}
System.out.println("===============================");
String[] strs = new String[3];
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
// 采用静态初始化的方式
String[] strs2 = {"abc", "def", "xyz"};
for (int i = 0; i < strs2.length; i++) {
System.out.println(strs2[i]);
}
// 存储Object,采用静态初始化呢?
/*Object o1 = new Object();
Object o2 = new Object();
Object o3 = new Object();
Object[] objects = {o1, o2, o3};*/
Object[] objects = {new Object(), new Object(), new Object()};
for (int i = 0; i < objects.length; i++) {
/*Object o = objects[i];
System.out.println(o);*/
System.out.println(objects[i]);
}
}
}
方法的参数是数组
public class ArrayTest03 {
// main方法的编写方式,还可以采用C++的语法格式哦!
public static void main(String args[]) {
// 调用方法时传一个数组
int[] x = {1,2,3,4};
printArray(x);
// 创建String数组
String[] stringArray = {"abc", "def", "hehe", "haha"};
printArray(stringArray);
String[] strArray = new String[10];
printArray(strArray); // 10个null
System.out.println("================================");
printArray(new String[3]);
System.out.println("***********************************");
printArray(new int[4]);
}
public static void printArray(int[] array){
for(int i = 0; i < array.length; i++){
System.out.println(array[i]);
}
}
public static void printArray(String[] args){
for(int i = 0; i < args.length; i++){
System.out.println("String数组中的元素:" + args[i]);
}
}
}
public class ArrayTest04 {
public static void main(String[] args) {
// 静态初始化一维数组
int[] a = {1,2,3};
printArray(a);
System.out.println("============================");
// 没有这种语法。
//printArray({1,2,3});
// 如果直接传递一个静态数组的话,语法必须这样写。
printArray(new int[]{1,2,3});
// 动态初始化一维数组
int[] a2 = new int[4];
printArray(a2);
System.out.println("=============================");
printArray(new int[3]);
}
// 为什么要使用静态方法?方便呀,不需要new对象啊。
public static void printArray(int[] array){
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}
main方法的String数组
main方法上面的“String[] args”有什么用?
分析一下:谁负责调用main方法(JVM)
JVM调用main方法的时候,会自动传一个String数组过来。
public class ArrayTest05 {
// 这个方法程序员负责写出来,JVM负责调用。JVM调用的时候一定会传一个String数组过来。
public static void main(String[] args) {
// JVM默认传递过来的这个数组对象的长度?默认0
// 通过测试得出:args不是null。
System.out.println("JVM给传递过来的String数组参数,它这个数组的长度是?" + args.length);
// 以下这一行代码表示的含义:数组对象创建了,但是数组中没有任何数据。
//String[] strs = new String[0];
//String[] strs = {}; // 静态初始化数组,里面没东西。
//printLength(strs);
// 这个数组什么时候里面会有值呢?
// 其实这个数组是留给用户的,用户可以在控制台上输入参数,这个参数自动会被转换为“String[] args”
// 例如这样运行程序:java ArrayTest05 abc def xyz
// 那么这个时候JVM会自动将“abc def xyz”通过空格的方式进行分离,分离完成之后,自动放到“String[] args”数组当中。
// 所以main方法上面的String[] args数组主要是用来接收用户输入参数的。
// 把abc def xyz 转换成字符串数组:{"abc","def","xyz"}
// 遍历数组
for (int i = 0; i < args.length; i++) {
System.out.println(args[i]);
}
}
public static void printLength(String[] args){
System.out.println(args.length); // 0
}
}
数组中存储引用数据类型
对于数组来说,实际上只能存储java对象的“内存地址”。数组中存储的每个元素是“引用”。
public class ArrayTest07 {
public static void main(String[] args) {
// 创建一个Animal类型的数组
Animal a1 = new Animal();
Animal a2 = new Animal();
Animal[] animals = {a1, a2};
// 对Animal数组进行遍历
for (int i = 0; i < animals.length; i++) {
/*Animal a = animals[i];
a.move();*/
// 代码合并
animals[i].move(); // 这个move()方法不是数组的。是数组当中Animal对象的move()方法。
}
// 动态初始化一个长度为2的Animal类型数组。
Animal[] ans = new Animal[2];
// 创建一个Animal对象,放到数组的第一个盒子中。
ans[0] = new Animal();
// Animal数组中只能存放Animal类型,不能存放Product类型。
//ans[1] = new Product();
// Animal数组中可以存放Cat类型的数据,因为Cat是一个Animal。
// Cat是Animal的子类。
ans[1] = new Cat();
// 创建一个Animal类型的数组,数组当中存储Cat和Bird
Cat c = new Cat();
Bird b = new Bird();
Animal[] anis = {c, b};
//Animal[] anis = {new Cat(), new Bird()}; // 该数组中存储了两个对象的内存地址。
for (int i = 0; i < anis.length; i++){
// 这个取出来的可能是Cat,也可能是Bird,不过肯定是一个Animal
// 如果调用的方法是父类中存在的方法不需要向下转型。直接使用父类型引用调用即可。
//anis[i]
//Animal an = anis[i];
//an.move();
//Animal中没有sing()方法。
//anis[i].sing();
// 调用子对象特有方法的话,需要向下转型!!!
if(anis[i] instanceof Cat){
Cat cat = (Cat)anis[i];
cat.catchMouse();
}else if(anis[i] instanceof Bird){
Bird bird = (Bird)anis[i];
bird.sing();
}
}
}
}
class Animal{
public void move(){
System.out.println("Animal move...");
}
}
// 商品类
class Product{
}
// Cat是子类
class Cat extends Animal {
public void move(){
System.out.println("猫在走猫步!");
}
// 特有方法
public void catchMouse(){
System.out.println("猫抓老鼠!");
}
}
// Bird子类
class Bird extends Animal {
public void move(){
System.out.println("Bird Fly!!!");
}
// 特有的方法
public void sing(){
System.out.println("鸟儿在歌唱!!!");
}
}
数组扩容
在java开发中,数组长度一旦确定不可变,那么数组满了怎么办?
数组满了,需要扩容。
java中对数组的扩容是:
先创建一个大容量的数组,然后将小容量数组中的数据一个一个拷贝到大数组当中。
public class ArrayTest08 {
public static void main(String[] args) {
// java中的数组是怎么进行拷贝的呢?
//System.arraycopy(5个参数);
// 拷贝源(从这个数组中拷贝)
int[] src = {1, 11, 22, 3, 4};
// 拷贝目标(拷贝到这个目标数组上)
int[] dest = new int[20]; // 动态初始化一个长度为20的数组,每一个元素默认值0
// 调用JDK System类中的arraycopy方法,来完成数组的拷贝
//System.arraycopy(src, 1, dest, 3, 2);
// 遍历目标数组
/*
for (int i = 0; i < dest.length; i++) {
System.out.println(dest[i]); // 0 0 0 11 22 ... 0
}
*/
System.arraycopy(src, 0, dest, 0, src.length);
for (int i = 0; i < dest.length; i++) {
System.out.println(dest[i]);
}
// 数组中如果存储的元素是引用,可以拷贝吗?当然可以。
String[] strs = {"hello", "world!", "study", "java", "oracle", "mysql", "jdbc"};
String[] newStrs = new String[20];
System.arraycopy(strs, 0, newStrs, 0, strs.length);
for (int i = 0; i < newStrs.length; i++) {
System.out.println(newStrs[i]);
}
System.out.println("================================");
Object[] objs = {new Object(), new Object(), new Object()};
Object[] newObjs = new Object[5];
// 思考一下:这里拷贝的时候是拷贝对象,还是拷贝对象的地址。(地址。)
System.arraycopy(objs, 0, newObjs, 0, objs.length);
for (int i = 0; i < newObjs.length; i++) {
System.out.println(newObjs[i]);
}
}
}
结论:数组扩容效率较低。因为涉及到拷贝的问题。所以在以后的开发中请注意:尽可能少的进行数组的拷贝。可以在创建数组对象的时候预估多长合适,最好预估准确,这样可以减少数组的扩容次数,提高效率。
数组模拟栈数据结构
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
5、假设栈的默认初始化容量是10.(请注意无参数构造方法的编写方式。)
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 因为数组是我们学习java的第一个容器。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
// new Animal()对象可以放进去,new Person()对象也可以放进去。因为Animal和Person的超级父类就是Object。
// 包括String也可以存储进去。因为String父类也是Object。
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj){
if(index >= elements.length - 1){
System.out.println("压栈失败,栈已满!");
return;
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop(){
if(index < 0){
System.out.println("弹栈失败,栈已空!");
return;
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
二维数组
1、二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每一个元素是一个一维数组。
2、三维数组是什么?
三维数组是一个特殊的二维数组,特殊在这个二维数组中每一个元素是一个一维数组。
实际开发中使用最多的就是一维数组。二维数组也很少使用。三维数组几乎不用。
3、二维数组静态初始化
int[][] array = {{1,1,1},{2,3,4,5},{0,0,0,0},{2,3,4,5},{2,3,4,5},{2,3,4,5},{2,3,4,5}};
public class ArrayTest09 {
public static void main(String[] args) {
// 一维数组
int[] array = {100, 200, 300};
System.out.println(array.length); // 3
System.out.println("=======================");
// 二维数组
// 以下代码当中:里面的是4个一维数组。
int[][] a = {
{100, 200, 300},
{30, 20, 40, 50, 60},
{6, 7, 9, 1},
{0}
};
System.out.println(a.length); // 4
System.out.println(a[0].length); // 3
System.out.println(a[1].length); // 5
System.out.println(a[2].length); // 4
System.out.println(a[3].length); // 1
// 里面的是5个一维数组。
int[][] a2 = {
{100, 200, 300},
{30, 20, 40, 50, 60},
{6, 7, 9, 1},
{0},
{1,2,3,4,5}
};
}
}
二维数组的元素访问
public class ArrayTest10 {
public static void main(String[] args) {
// 二维数组
int[][] a = {
{34,4,65},
{100,200,3900,111},
{0}
};
// 请取出以上二维数组中的第1个一维数组。
int[] 我是第1个一维数组 = a[0];
int 我是第1个一维数组中的第1个元素 = 我是第1个一维数组[0];
System.out.println(我是第1个一维数组中的第1个元素);
// 以下代码的由来是因为以上代码的合并导致的。
System.out.println(a[0][0]);
// 取出第2个一维数组当中第3个元素
System.out.println("第二个一维数组中第三个元素:" + a[1][2]);
// 取出第3个一维数组当中第1个元素
System.out.println("第3个一维数组中第1个元素:" + a[2][0]);
// 改
a[2][0] = 11111;
System.out.println(a[2][0]);
// 注意别越界。
//java.lang.ArrayIndexOutOfBoundsException
//System.out.println(a[2][1]);
}
}
遍历二维数组
public class ArrayTest11 {
public static void main(String[] args) {
// 二维数组
String[][] array = {
{"java", "oracle", "c++", "python", "c#"},
{"张三", "李四", "王五"},
{"lucy", "jack", "rose"}
};
// 遍历二维数组
for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)
String[] 一维数组 = array[i];
// 负责遍历一维数组
for(int j = 0; j < 一维数组.length; j++){
System.out.print(一维数组[j] + " ");
}
// 输出换行符
System.out.println();
}
// 合并代码
for(int i = 0; i < array.length; i++){ // 外层循环3次。(负责纵向。)
for(int j = 0; j < array[i].length; j++){
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
}
方法的参数是二维数组
public class ArrayTest12 {
public static void main(String[] args) {
// 3行4列。
// 3个一维数组,每一个一维数组当中4个元素。
int[][] array = new int[3][4];
// 二维数组遍历
/*
for (int i = 0; i < array.length; i++) { // 循环3次。
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
*/
// 静态初始化
int[][] a = {{1,2,3,4},{4,5,6,76},{1,23,4}};
printArray(a);
// 没有这种语法
//printArray({{1,2,3,4},{4,5,6,76},{1,23,4}});
// 可以这样写。
printArray(new int[][]{{1,2,3,4},{4,5,6,76},{1,23,4}});
}
public static void printArray(int[][] array){
// 遍历二维数组。
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
System.out.print(array[i][j] + " ");
}
System.out.println();
}
}
}
冒泡排序算法
1、每一次循环结束之后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡。)
2、核心:
拿着左边的数字和右边的数字比对,当左边 > 右边的时候,交换位置。
例:
public class BubbleSort {
public static void main(String[] args) {
// 这是int类型的数组对象
//int[] arr = {3, 2, 7, 6, 8};
int[] arr = {9, 8, 10, 7, 6, 0, 11};
// 7条数据,循环6次。以下的代码可以循环6次。
/*
for(int i = 0; i < arr.length-1; i++){
System.out.println(i);
}
*/
// 7条数据,循环6次。以下的代码可以循环6次。(冒泡排序的外层循环采用这种方式)
//int count = 0;
int count2 = 0;
for(int i = arr.length-1; i > 0; i--){
for(int j = 0; j < i; j++){
// 不管是否需要交换位置,总之是要比较一次的。
//count++;
if(arr[j] > arr[j+1]){
// 交换位置。
// arr[j] 和 arr[j+1] 交换
int temp;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
count2++;
}
}
}
//System.out.println("比较次数:" + count);
System.out.println("交换位置的次数:" + count2); //13
// 输出结果
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
选择排序算法
1、每一次从这堆“参与比较的数据当中”找出最小值。
2、拿着这个最小值和“参与比较的这堆最前面的元素”交换位置。
选择排序比冒泡排序好在:每一次的交换位置都是有意义的。
选择排序比冒泡排序的效率高:高在交换位置的次数上。
public class SelectSort {
public static void main(String[] args) {
//int[] arr = {3, 1, 6, 2, 5};
int[] arr = {9, 8, 10, 7, 6, 0, 11};
int count = 0;
int count2 = 0;
// 选择排序
// 5条数据循环4次。(外层循环4次。)
for(int i = 0; i < arr.length - 1; i++){
// i的值是0 1 2 3
// i正好是“参加比较的这堆数据中”最左边那个元素的下标。
//System.out.println(i);
// i是一个参与比较的这堆数据中的起点下标。
// 假设起点i下标位置上的元素是最小的。
int min = i;
for(int j = i+1; j < arr.length; j++){
count++;
//System.out.println("===>" + j);
if(arr[j] < arr[min]){
min = j; //最小值的元素下标是j
}
}
// 当i和min相等时,表示最初猜测是对的。
// 当i和min不相等时,表示最初猜测是错的,有比这个元素更小的元素,
// 需要拿着这个更小的元素和最左边的元素交换位置。
if(min != i){
// 表示存在更小的数据
// arr[min] 最小的数据
// arr[i] 最前面的数据
int temp;
temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
count2++;
}
}
// 冒泡排序和选择排序实际上比较的次数没变。
// 交换位置的次数减少了。
System.out.println("比较次数" + count); // 21
System.out.println("交换次数:" + count2); // 5
// 排序之后遍历
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
二分法(折半法)查找
1、二分法查找算法是基于排序的基础之上。(没有排序的数组是无法查找的)
2、二分法查找的终止条件:一直折半,直到中间的那个元素恰好是被查找的元素。
3、二分法查找的循环条件:开始下标小于等于结束下标。
例:
例:
public class ArrayUtil {
public static void main(String[] args) {
int[] arr = {100,200,230,235,600,1000,2000,9999};
// 找出arr这个数组中200所在的下标。
// 调用方法
int index = binarySearch(arr, 230);
System.out.println(index == -1 ? "该元素不存在!" : "该元素下标" + index);
}
/**
* 从数组中查找目标元素的下标。
* @param arr 被查找的数组(这个必须是已经排序的。)
* @param dest 目标元素
* @return -1表示该元素不存在,其它表示返回该元素的下标。
*/
public static int binarySearch(int[] arr, int dest) {
// 开始下标
int begin = 0;
// 结束下标
int end = arr.length - 1;
// 开始元素的下标只要在结束元素下标的左边,就有机会继续循环。
while(begin <= end) {
// 中间元素下标
int mid = (begin + end) / 2;
if (arr[mid] == dest) {
return mid;
} else if (arr[mid] < dest) {
// 目标在“中间”的右边
// 开始元素下标需要发生变化(开始元素的下标需要重新赋值)
begin = mid + 1; // 一直增
} else {
// arr[mid] > dest
// 目标在“中间”的左边
// 修改结束元素的下标
end = mid - 1; // 一直减
}
}
return -1;
}
}
String字符串
Java JDK中内置的一个类:java.lang.String
1、String表示字符串类型,属于引用数据类型,不属于基本数据类型。
2、在java中随便使用双引号括起来的都是String对象。例如:“abc”,“def”,“hello world!”,这是3个String对象
3、java中规定,双引号括起来的字符串,是不可变的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab"。
4、在JDK当中双引号括起来的字符串,例如:“abc” "def"都是直接存储在“方法区”的“字符串常量池”当中。
为什么SUN公司把字符串存储在一个“字符串常量池”当中呢?因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。
存储原理
例1:
public class StringTest01 {
public static void main(String[] args) {
// 这两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
// 分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
// 凡是双引号括起来的都在字符串常量池中有一份。
// new对象的时候一定在堆内存当中开辟空间。
String s3 = new String("xy");
// i变量中保存的是100这个值。
int i = 100;
// s变量中保存的是字符串对象的内存地址。
// s引用中保存的不是"abc",是0x1111
// 而0x1111是"abc"字符串对象在“字符串常量池”当中的内存地址。
String s = "abc";
}
}
例2:
public class UserTest {
public static void main(String[] args) {
User user = new User(110, "张三");
}
}
例3:
public class StringTest02 {
public static void main(String[] args) {
String s1 = "hello";
// "hello"是存储在方法区的字符串常量池当中
// 所以这个"hello"不会新建。(因为这个对象已经存在了!)
String s2 = "hello";
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(s1 == s2); // true
String x = new String("xyz");
String y = new String("xyz");
// 分析结果是true还是false?
// == 双等号比较的是不是变量中保存的内存地址?是的。
System.out.println(x == y); //false
// 通过这个案例的学习,我们知道了,字符串对象之间的比较不能使用“==”
// "=="不保险。应该调用String类的equals方法。
// String类已经重写了equals方法,以下的equals方法调用的是String重写之后的equals方法。
System.out.println(x.equals(y)); // true
String k = new String("testString");
//String k = null;
// "testString"这个字符串可以后面加"."呢?
// 因为"testString"是一个String字符串对象。只要是对象都能调用方法。
System.out.println("testString".equals(k)); // 建议使用这种方式,因为这个可以避免空指针异常。
System.out.println(k.equals("testString")); // 存在空指针异常的风险。不建议这样写。
}
}
例4:(面试题)
/*
分析以下程序,一共创建了几个对象
*/
public class StringTest03 {
public static void main(String[] args) {
/*
一共3个对象:
方法区字符串常量池中有1个:"hello"
堆内存当中有两个String对象。
一共3个。
*/
String s1 = new String("hello");
String s2 = new String("hello");
}
}
构造方法
常用的构造方法:
String s = new String("");
String s = ""; //最常用
String s = new String(char数组);
String s = new String(char数组,起始下标,长度);
String s = new String(byte数组);
String s = new String(byte数组,起始下标,长度);
public class StringTest04 {
public static void main(String[] args) {
// 创建字符串对象最常用的一种方式
String s1 = "hello world!";
// s1这个变量中保存的是一个内存地址。
// 按说以下应该输出一个地址。
// 但是输出一个字符串,说明String类已经重写了toString()方法。
System.out.println(s1);//hello world!
System.out.println(s1.toString()); //hello world!
// 这里只掌握常用的构造方法。
byte[] bytes = {97, 98, 99}; // 97是a,98是b,99是c
String s2 = new String(bytes);
// 前面说过:输出一个引用的时候,会自动调用toString()方法,默认Object的话,会自动输出对象的内存地址。
// 通过输出结果我们得出一个结论:String类已经重写了toString()方法。
// 输出字符串对象的话,输出的不是对象的内存地址,而是字符串本身。
System.out.println(s2.toString()); //abc
System.out.println(s2); //abc
// String(字节数组,数组元素下标的起始位置,长度)
// 将byte数组中的一部分转换成字符串。
String s3 = new String(bytes, 1, 2);
System.out.println(s3); // bc
// 将char数组全部转换成字符串
char[] chars = {'我','是','中','国','人'};
String s4 = new String(chars);
System.out.println(s4);
// 将char数组的一部分转换成字符串
String s5 = new String(chars, 2, 3);
System.out.println(s5);
String s6 = new String("helloworld!");
System.out.println(s6); //helloworld!
}
}
常用方法
charAt
//1(掌握).char charAt(int index)
char c = "中国人".charAt(1); // "中国人"是一个字符串String对象。只要是对象就能“点.”
System.out.println(c); // 国
compareTo
// 2(了解).int compareTo(String anotherString)
// 字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。
int result = "abc".compareTo("abc");
System.out.println(result); //0(等于0) 前后一致 10 - 10 = 0
int result2 = "abcd".compareTo("abce");
System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1
int result3 = "abce".compareTo("abcd");
System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1
// 拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。
System.out.println("xyz".compareTo("yxz")); // -1
contains
// 3(掌握).boolean contains(CharSequence s)
// 判断前面的字符串中是否包含后面的子字符串。
System.out.println("HelloWorld.java".contains(".java")); // true
System.out.println("http://www.baidu.com".contains("https://")); // false
endsWith
// 4(掌握). boolean endsWith(String suffix)
// 判断当前字符串是否以某个子字符串结尾。
System.out.println("test.txt".endsWith(".java")); // false
System.out.println("test.txt".endsWith(".txt")); // true
System.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true
equals
// 5(掌握).boolean equals(Object anObject)
// 比较两个字符串必须使用equals方法,不能使用“==”
// equals方法有没有调用compareTo方法? 老版本可以看一下。JDK13中并没有调用compareTo()方法。
// equals只能看出相等不相等。
// compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
System.out.println("abc".equals("abc")); // true
equalsIgnoreCase
// 6(掌握).boolean equalsIgnoreCase(String anotherString)
// 判断两个字符串是否相等,并且同时忽略大小写。
System.out.println("ABc".equalsIgnoreCase("abC")); // true
getBytes
// 7(掌握).byte[] getBytes()
// 将字符串对象转换成字节数组
byte[] bytes = "abcdef".getBytes();
for(int i = 0; i < bytes.length; i++){
System.out.println(bytes[i]); //输出97 98 99 100 101 102
}
indexOf
// 8(掌握).int indexOf(String str)
// 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。
System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); // 6
isEmpty
// 9(掌握).boolean isEmpty()
// 判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。
//String s = "";
String s = "a";
System.out.println(s.isEmpty());
length
// 10(掌握). int length()
// 面试题:判断数组长度和判断字符串长度不一样
// 查看数组长度是length属性,查看字符串长度是length()方法。
System.out.println("abc".length()); // 3
System.out.println("".length()); // 0
lastIndexOf
// 11(掌握).int lastIndexOf(String str)
// 判断某个子字符串在当前字符串中最后一次出现的索引(下标)
System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22
replace
// 12(掌握). String replace(CharSequence target, CharSequence replacement)
// 替换。
// String的父接口就是:CharSequence
String newString = "http://www.baidu.com".replace("http://", "https://");
System.out.println(newString); //https://www.baidu.com
// 把以下字符串中的“=”替换成“:”
String newString2 = "name=zhangsan&password=123&age=20".replace("=", ":");
System.out.println(newString2); //name:zhangsan&password:123&age:20
split
// 13(掌握).String[] split(String regex)
// 拆分字符串
String[] ymd = "1980-10-11".split("-"); //"1980-10-11"以"-"分隔符进行拆分。
for(int i = 0; i < ymd.length; i++){
System.out.println(ymd[i]);
}
String param = "name=zhangsan&password=123&age=20";
String[] params = param.split("&");
for(int i = 0; i <params.length; i++){
System.out.println(params[i]);
// 可以继续向下拆分,可以通过“=”拆分。
}
startsWith
// 14(掌握)、boolean startsWith(String prefix)
// 判断某个字符串是否以某个子字符串开始。
System.out.println("http://www.baidu.com".startsWith("http")); // true
System.out.println("http://www.baidu.com".startsWith("https")); // false
substring
// 15(掌握)、 String substring(int beginIndex) 参数是起始下标。
// 截取字符串
System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com
// 16(掌握)、String substring(int beginIndex, int endIndex)
// beginIndex起始位置(包括)
// endIndex结束位置(不包括)
System.out.println("http://www.baidu.com".substring(7, 10)); //www
toCharArray
// 17(掌握)、char[] toCharArray()
// 将字符串转换成char数组
char[] chars = "我是中国人".toCharArray();
for(int i = 0; i < chars.length; i++){
System.out.println(chars[i]);
}
toLowerCase和toUpperCase
// 18(掌握)、String toLowerCase()
// 转换为小写。
System.out.println("ABCDefKXyz".toLowerCase());
// 19(掌握)、String toUpperCase();
System.out.println("ABCDefKXyz".toUpperCase());
trim
// 20(掌握). String trim();
// 去除字符串前后空白
System.out.println(" hello world ".trim());
静态方法valueOf
// 21(掌握). String中只有一个方法是静态的,不需要new对象
// 这个方法叫做valueOf
// 作用:将“非字符串”转换成“字符串”
//String s1 = String.valueOf(true);
//String s1 = String.valueOf(100);
//String s1 = String.valueOf(3.14);
// 这个静态的valueOf()方法,参数是一个对象的时候,会自动调用该对象的toString()方法吗?
String s1 = String.valueOf(new Customer());
//System.out.println(s1); // 没有重写toString()方法之前是对象内存地址 com.bjpowernode.javase.string.Customer@10f87f48
System.out.println(s1); //我是一个VIP客户!!!!
// 我们是不是可以研究一下println()方法的源代码了?
System.out.println(100);
System.out.println(3.14);
System.out.println(true);
Object obj = new Object();
// 通过源代码可以看出:为什么输出一个引用的时候,会调用toString()方法!!!!
// 本质上System.out.println()这个方法在输出任何数据的时候都是先转换成字符串,再输出。
System.out.println(obj);
System.out.println(new Customer());
StringBuffer
思考:我们在实际的开发中,如果需要进行字符串的频繁拼接,会有什么问题?
因为java中的字符串是不可变的,每一次拼接都会产生新字符串。
这样会占用大量的方法区内存,造成内存空间的浪费。
String s = “abc”;
s += “hello”;
就以上两行代码,就导致在方法区字符串常量池当中创建了3个对象:
“abc” “hello” “abchello”
如果以后需要进行大量字符串的拼接操作,建议使用JDK中自带的:
java.lang.StringBuffer
java.lang.StringBuilder
public class StringBufferTest02 {
public static void main(String[] args) {
// 创建一个初始化容量为16个byte[] 数组。(字符串缓冲区对象)
StringBuffer stringBuffer = new StringBuffer();
// 拼接字符串,以后拼接字符串统一调用 append()方法。
// append是追加的意思。
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("d");
stringBuffer.append(3.14);
stringBuffer.append(true);
// append方法底层在进行追加的时候,如果byte数组满了,会自动扩容。
stringBuffer.append(100L);
System.out.println(stringBuffer.toString());
// 指定初始化容量的StringBuffer对象(字符串缓冲区对象)
StringBuffer sb = new StringBuffer(100);
sb.append("hello");
sb.append("world");
sb.append("hello");
sb.append("kitty");
System.out.println(sb);
}
}
如何优化StringBuffer的性能?
在创建StringBuffer的时候尽可能给定一个初始化容量。
最好减少底层数组的扩容次数。预估一下,给一个大一些的初始化容量。
关键点:给一个合适的初始化容量,可以提高程序的执行效率。(无参构造方法中默认是给定数组长度16)
StringBuilder
StringBuffer和StringBuilder的区别?
StringBuffer中的方法都有synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
StringBuilder中的方法都没有synchronized关键字修饰。表示StringBuilder在多线程环境下运行是不安全的。
StringBuffer是线程安全的。
StringBuilder是非线程安全的。
作用上基本和StringBuffer一样。
public class StringBuilderTest01 {
public static void main(String[] args) {
// 使用StringBuilder也是可以完成字符串的拼接。
StringBuilder sb = new StringBuilder();
sb.append(100);
sb.append(true);
sb.append("hello");
sb.append("kitty");
System.out.println(sb);
}
}
String为什么是不可变的?
面试题:String为什么是不可变的?
String类中有一个byte[]数组,这个byte[]数组采用了final修饰,因为数组一旦创建长度不可变,并且被final修饰的引用一旦指向某个对象之后,不可再指向其它对象,所以String是不可变的!
StringBuffer/StringBuilder为什么是可变的呢?
StringBuffer/StringBuilder内部实际上是一个byte[]数组,这个byte[]数组没有被final修饰,StringBuffer/StringBuilder的初始化容量是16,当存满之后会进行扩容,底层调用了数组拷贝的方法System.arraycopy()…。所以StirngBuffer/StringBuilder适合于使用字符串的频繁拼接操作。
拼接就是往byte[]数组中继续放拼接字符串拆分后的单个字符的字节,如果数组不够放了就扩容,进行数组复制,原来的数组在堆内存中就被垃圾回收了,byte[]数组引用指向新的数组对象。这样就不会浪费内存空间!!!
String不可变就是因为底层是final修饰的数组变量,byte[]数组引用不能指向新的数组对象了!!!
public class StringBufferTest04 {
public static void main(String[] args) {
// 字符串不可变是什么意思?
// 是说双引号里面的字符串对象一旦创建不可变。
String s = "abc"; //"abc"放到了字符串常量池当中。"abc"不可变。
// s变量是可以指向其它对象的。
// 字符串不可变不是说以上变量s不可变。说的是"abc"这个对象不可变。
s = "xyz";//"xyz"放到了字符串常量池当中。"xyz"不可变。
}
}
包装类
java中为8种基本数据类型又对应准备了8种包装类型。8种包装类属于引用数据类型,父类是Object。
包装类存在的意义
思考:为什么要再提供8种包装类呢?
因为8种基本数据类型不够用。
所以SUN又提供对应的8种包装类型。
public class IntegerTest01 {
//入口
public static void main(String[] args) {
// 有没有这种需求:调用doSome()方法的时候需要传一个数字进去。
// 但是数字属于基本数据类型,而doSome()方法参数的类型是Object。
// 可见doSome()方法无法接收基本数据类型的数字。那怎么办呢?可以传一个数字对应的包装类进去。
// 把100这个数字经过构造方法包装成对象。
MyInt myInt = new MyInt(100);
// doSome()方法虽然不能直接传100,但是可以传一个100对应的包装类型。
doSome(myInt);
}
public static void doSome(Object obj){
//System.out.println(obj);
System.out.println(obj.toString());
}
}
// 这种包装类目前是我自己写的。实际开发中我们不需要自己写。
// 8种基本数据类型对应的8种包装类,SUN公司已经写好了。我们直接用。
public class MyInt {
int value;
public MyInt() {
}
public MyInt(int value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
八种包装类
8种基本数据类型对应的包装类型名是什么?
基本数据类型 | 包装类型 |
---|---|
byte | java.lang.Byte(父类Number) |
short | java.lang.Short(父类Number) |
int | java.lang.Integer(父类Number) |
long | java.lang.Long(父类Number) |
float | java.lang.Float(父类Number) |
double | java.lang.Double(父类Number) |
boolean | java.lang.Boolean(父类Object) |
char | java.lang.Character(父类Object) |
Number类中的公共方法
八种包装类中其中6个都是数字对应的包装类,他们的父类都是Number,研究一下Number中公共的方法:
Number是一个抽象类,无法实例化对象。t法
Number类中有这样的方法:
byte byteValue() :以byte形式返回指定的数值。
abstract double doubleValue() :以double形式返回指定的数值。
abstract float floatValue() :以float形式返回指定的数值。
abstract int intValue() :以int形式返回指定的数值。
abstract long longValue() :以long形式返回指定的数值。
short shortValue() :以short形式返回指定的数值。
这些方法所有的数字包装类的子类都有,这些方法是负责拆箱的。
装箱和拆箱的概念
装箱:基本数据类型 --> 引用数据类型
拆箱:引用数据类型 --> 基本数据类型
public class IntegerTest02 {
public static void main(String[] args) {
// 123这个基本数据类型,进行构造方法的包装达到了:基本数据类型向引用数据类型的转换。
// 基本数据类型 -(转换为)->引用数据类型(装箱)
Integer i = new Integer(123);
// 将引用数据类型--(转换为)-> 基本数据类型
float f = i.floatValue();
System.out.println(f); //123.0
// 将引用数据类型--(转换为)-> 基本数据类型(拆箱)
int retValue = i.intValue();
System.out.println(retValue); //123
}
}
Integer的构造方法
Integer类的构造方法,有两个:
Integer(int)
Integer(String)
public class IntegerTest03 {
public static void main(String[] args) {
// Java9之后不建议使用这个构造方法了。出现横线表示已过时。
// 将数字100转换成Integer包装类型(int --> Integer)
Integer x = new Integer(100);
System.out.println(x);
// 将String类型的数字,转换成Integer包装类型。(String --> Integer)
Integer y = new Integer("123");
System.out.println(y);
// double -->Double
Double d = new Double(1.23);
System.out.println(d);
// String --> Double
Double e = new Double("3.14");
System.out.println(e);
}
}
通过常量获取最大值和最小值
public class IntegerTest04 {
public static void main(String[] args) {
// 通过访问包装类的常量,来获取最大值和最小值
System.out.println("int的最大值:" + Integer.MAX_VALUE);
System.out.println("int的最小值:" + Integer.MIN_VALUE);
System.out.println("byte的最大值:" + Byte.MAX_VALUE);
System.out.println("byte的最小值:" + Byte.MIN_VALUE);
}
}
自动装箱和自动拆箱
好消息:在java5之后,引入了一种新特性,自动装箱和自动拆箱。
自动装箱:基本数据类型自动转换成包装类。
自动拆箱:包装类自动转换成基本数据类型。
有了自动拆箱之后,Number类中的方法就用不着了!
自动装箱和自动拆箱的好处?方便编程。
public class IntegerTest05 {
public static void main(String[] args) {
// 900是基本数据类型
// x是包装类型
// 基本数据类型 --(自动转换)--> 包装类型:自动装箱
Integer x = 900;
System.out.println(x);
// x是包装类型
// y是基本数据类型
// 包装类型 --(自动转换)--> 基本数据类型:自动拆箱
int y = x;
System.out.println(y);
// z是一个引用,z是一个变量,z还是保存了一个对象的内存地址。
Integer z = 1000; // 等同于:Integer z = new Integer(1000);
// 分析为什么这个没有报错呢?
// +两边要求是基本数据类型的数字,z是包装类,不属于基本数据类型,这里会进行自动拆箱。将z转换成基本数据类型
// 在java5之前你这样写肯定编译器报错。
System.out.println(z + 1);
Integer a = 1000; // Integer a = new Integer(1000); a是个引用,保存内存地址指向对象。
Integer b = 1000; // Integer b = new Integer(1000); b是个引用,保存内存地址指向对象。
// == 比较的是对象的内存地址,a和b两个引用中保存的对象内存地址不同。
// == 这个运算符不会触发自动拆箱机制。(只有+ - * /等运算的时候才会。)
System.out.println(a == b); //false
}
}
java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据不需要再new了,直接从整数型常量池当中取出来。
面试题:(非常重要)
/*
这个题目是Integer非常重要的面试题。
*/
public class IntegerTest06 {
public static void main(String[] args) {
Integer a = 128;
Integer b = 128;
System.out.println(a == b); //false
/*
java中为了提高程序的执行效率,将[-128到127]之间所有的包装对象提前创建好,
放到了一个方法区的“整数型常量池”当中了,目的是只要用这个区间的数据不需要
再new了,直接从整数型常量池当中取出来。
原理:x变量中保存的对象的内存地址和y变量中保存的对象的内存地址是一样的。
*/
Integer x = 127;
Integer y = 127;
// == 永远判断的都是两个对象的内存地址是否相同。
System.out.println(x == y); //true
}
}
Integer常用方法
parseInt
// 重点方法
// static int parseInt(String s)
// 静态方法,传参String,返回int
//网页上文本框中输入的100实际上是"100"字符串。后台数据库中要求存储100数字,此时java程序需要将"100"转换成100数字。
int retValue = Integer.parseInt("123"); // String -转换-> int
//int retValue = Integer.parseInt("中文"); // NumberFormatException
System.out.println(retValue + 100);
// 照葫芦画瓢
double retValue2 = Double.parseDouble("3.14");
System.out.println(retValue2 + 1); //4.140000000000001(精度问题)
float retValue3 = Float.parseFloat("1.0");
System.out.println(retValue3 + 1); //2.0
toBinaryString
// static String toBinaryString(int i)
// 静态的:将十进制转换成二进制字符串。
String binaryString = Integer.toBinaryString(3);
System.out.println(binaryString); //"11" 二进制字符串
toHexString
// static String toHexString(int i)
// 静态的:将十进制转换成十六进制字符串。
String hexString = Integer.toHexString(16);
System.out.println(hexString); // "10"
// 十六进制:1 2 3 4 5 6 7 8 9 a b c d e f 10 11 12 13 14 15 16 17 18 19 1a
hexString = Integer.toHexString(17);
System.out.println(hexString); // "11"
toOctalString
//static String toOctalString(int i)
// 静态的:将十进制转换成八进制字符串。
String octalString = Integer.toOctalString(8);
System.out.println(octalString); // "10"
valueOf
// valueOf方法作为了解
//static Integer valueOf(int i)
// 静态的:int-->Integer
Integer i1 = Integer.valueOf(100);
System.out.println(i1);
// static Integer valueOf(String s)
// 静态的:String-->Integer
Integer i2 = Integer.valueOf("100");
System.out.println(i2);
总结一下之前的经典异常
空指针异常:NullPointerException
类型转换异常:ClassCastException
数组下标越界异常:ArrayIndexOutOfBoundsException
数字格式化异常:NumberFormatException
// 编译的时候没问题,一切符合java语法,运行时会不会出问题呢?
// 不是一个“数字”可以包装成Integer吗?不能。运行时出现异常。
// java.lang.NumberFormatException
Integer a = new Integer("中文");
String int Integer类型互换
对日期的处理
知识点1:怎么获取系统当前时间
知识点2:String —> Date
知识点3:Date —> String
1、获取系统当前时间(精确到毫秒的系统当前时间):
java.util.Date
// 获取系统当前时间(精确到毫秒的系统当前时间)
// 直接调用无参数构造方法就行。
Date nowTime = new Date();
// java.util.Date类的toString()方法已经被重写了。
// 输出的应该不是一个对象的内存地址,应该是一个日期字符串。
//System.out.println(nowTime); //Thu Mar 05 10:51:06 CST 2020
2、Date —> String
java.text.SimpleDateFormat
// 日期可以格式化吗?
// 将日期类型Date,按照指定的格式进行转换:Date --转换成具有一定格式的日期字符串-->String
// SimpleDateFormat是java.text包下的。专门负责日期格式化的。
/*
yyyy 年(年是4位)
MM 月(月是2位)
dd 日
HH 时
mm 分
ss 秒
SSS 毫秒(毫秒3位,最高999。1000毫秒代表1秒)
注意:在日期格式中,除了y M d H m s S这些字符不能随便写之外,剩下的符号格式自己随意组织。
*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
//SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
//SimpleDateFormat sdf = new SimpleDateFormat("yy/MM/dd HH:mm:ss");
String nowTimeStr = sdf.format(nowTime);
System.out.println(nowTimeStr);
3、String —> Date
// 假设现在有一个日期字符串String,怎么转换成Date类型?
// String --> Date
String time = "2008-08-08 08:08:08 888";
//SimpleDateFormat sdf2 = new SimpleDateFormat("格式不能随便写,要和日期字符串格式相同");
// 注意:字符串的日期格式和SimpleDateFormat对象指定的日期格式要一致。不然会出现异常:java.text.ParseException
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date dateTime = sdf2.parse(time);
System.out.println(dateTime); //Fri Aug 08 08:08:08 CST 2008
统计方法执行时长
Syetem.currentTimeMillis()获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。
public class DateTest02 {
public static void main(String[] args) {
// 获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数。
long nowTimeMillis = System.currentTimeMillis();
System.out.println(nowTimeMillis); //1583377912981
// 统计一个方法耗时
// 在调用目标方法之前记录一个毫秒数
long begin = System.currentTimeMillis();
print();
// 在执行完目标方法之后记录一个毫秒数
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
// 需求:统计一个方法执行所耗费的时长
public static void print(){
for(int i = 0; i < 1000000000; i++){
System.out.println("i = " + i);
}
}
}
System类的相关属性和方法
System.out :out是System类的静态变量,类型是PrintStream
System.out.println() :println()方法不是System类的,是PrintStream类的方法。
System.gc() :建议启动垃圾回收器
System.currentTimeMillis() :获取自1970年1月1日到系统当前时间的总毫秒数
System.exit(0) :退出JVM
Date的构造方法
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTest03 {
public static void main(String[] args) {
// 这个时间是什么时间?
// 1970-01-01 00:00:00 001
Date time = new Date(1); // 注意:参数是一个毫秒
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String strTime = sdf.format(time);
// 北京是东8区。差8个小时。
System.out.println(strTime); // 1970-01-01 08:00:00 001
// 获取昨天的此时的时间。
Date time2 = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24);
String strTime2 = sdf.format(time2);
System.out.println(strTime2); //2020-03-04 11:44:14 829
// 获取“去年的今天”的时间
// 自己玩。
}
}
数字类
数字格式化
java.text.DecimalFormat
public class DecimalFormatTest01 {
public static void main(String[] args) {
// java.text.DecimalFormat专门负责数字格式化的。
//DecimalFormat df = new DecimalFormat("数字格式");
/*
数字格式有哪些?
# 代表任意数字
, 代表千分位
. 代表小数点
0 代表不够时补0
###,###.##
表示:加入千分位,保留2个小数。
*/
DecimalFormat df = new DecimalFormat("###,###.##");
//String s = df.format(1234.56);
String s = df.format(1234.561232);
System.out.println(s); // "1,234.56"
DecimalFormat df2 = new DecimalFormat("###,###.0000"); //保留4个小数位,不够补上0
String s2 = df2.format(1234.56);
System.out.println(s2); // "1,234.5600"
}
}
高精度BigDecimal
java.math.BigDecimal属于大数据,精度极高。不属于基本数据类型,属于java对象(引用数据类型)。
这是SUN提供的一个类,专门用在财务软件当中。
public class BigDecimalTest01 {
public static void main(String[] args) {
// 这个100不是普通的100,是精度极高的100
BigDecimal v1 = new BigDecimal(100);
// 精度极高的200
BigDecimal v2 = new BigDecimal(200);
// 求和
// v1 + v2; // 这样不行,v1和v2都是引用,不能直接使用+求和。
BigDecimal v3 = v1.add(v2); // 调用方法求和。
System.out.println(v3); //300
BigDecimal v4 = v2.divide(v1);
System.out.println(v4); // 2
}
}
随机数Random
java.util.Random
import java.util.Random;
public class RandomTest01 {
public static void main(String[] args) {
// 创建随机数对象
Random random = new Random();
// 随机产生一个int类型取值范围内的数字。
int num1 = random.nextInt();
System.out.println(num1);
// 产生[0~100]之间的随机数。不能产生101。
// nextInt翻译为:下一个int类型的数据是101,表示只能取到100.
int num2 = random.nextInt(101); //不包括101
System.out.println(num2);
}
}
例:编写程序,生成5个不重复的随机数[0-100]。重复的话重新生成。最终生成的5个随机数放到数组中,要求数组中这5个随机数不重复。
import java.util.Arrays;
import java.util.Random;
public class RandomTest02 {
public static void main(String[] args) {
// 创建Random对象
Random random = new Random();
// 准备一个长度为5的一维数组。
int[] arr = new int[5]; // 默认值都是0
for(int i = 0; i < arr.length; i++){
arr[i] = -1;
}
// 循环,生成随机数
int index = 0;
while(index < arr.length){
// 生成随机数
//int num = random.nextInt(101);
//int num = random.nextInt(6); // 只能生成[0-5]的随机数!
int num = random.nextInt(4); // 只能生成[0-3]的随机数!永远都有重复的,永远都凑不够5个。
System.out.println("生成的随机数:" + num);
// 判断arr数组中有没有这个num
// 如果没有这个num,就放进去。
if(!contains(arr, num)){
arr[index++] = num;
}
}
// 遍历以上的数组
for(int i = 0; i < arr.length; i++){
System.out.println(arr[i]);
}
}
/**
* 单独编写一个方法,这个方法专门用来判断数组中是否包含某个元素
* @param arr 数组
* @param key 元素
* @return true表示包含,false表示不包含。
*/
public static boolean contains(int[] arr, int key){
/*
// 这个方案bug。(排序出问题了。)
// 对数组进行升序
//Arrays.sort(arr);
// 进行二分法查找
// 二分法查找的结果 >= 0说明,这个元素找到了,找到了表示存在!
//return Arrays.binarySearch(arr, key) >= 0;
*/
for(int i = 0; i < arr.length; i++){
if(arr[i] == key){
// 条件成立了表示包含,返回true
return true;
}
}
// 这个就表示不包含!
return false;
}
}
枚举类型
为什么使用枚举类型
需求(这是设计者说的!):以下程序,计算两个int类型数据的商,计算成功返回1,计算失败返回0
@param a int类型的数据
@param b int类型的数据
@return 返回1表示成功,返回0表示失败!
public static int divide(int a, int b){
try {
int c = a / b;
// 程序执行到此处表示以上代码没有发生异常。表示执行成功!
return 1;
} catch (Exception e){
// 程序执行到此处表示以上程序出现了异常!
// 表示执行失败!
return 0;
}
}
*/
// 设计缺陷?在这个方法的返回值类型上。返回一个int不恰当。
// 既然最后的结果只是成功和失败,最好使用布尔类型。因为布尔类型true和false正好可以表示两种不同的状态。
/*
public static int divide(int a, int b){
try {
int c = a / b;
// 返回10已经偏离了需求,实际上已经出错了,但是编译器没有检查出来。
// 我们一直想追求的是:所有的错误尽可能让编译器找出来,所有的错误越早发现越好!
return 10;
} catch (Exception e){
return 0;
}
}
*/
// 这种设计不错。
public static boolean divide(int a, int b){
try {
int c = a / b;
return true;
} catch (Exception e){
return false;
}
}
/*
思考:以上的这个方法设计没毛病,挺好,返回true和false表示两种情况,
但是在以后的开发中,有可能遇到一个方法的执行结果可能包括三种情况,
四种情况,五种情况不等,但是每一个都是可以数清楚的,一枚一枚都是可以
列举出来的。这个布尔类型就无法满足需求了。此时需要使用java语言中的
枚举类型。
*/
枚举类型的使用
总结:
1、枚举是一种引用数据类型
2、枚举类型怎么定义,语法是?
enum 枚举类型名{
枚举值1,枚举值2
}
3、结果只有两种情况的,建议使用布尔类型。
结果超过两种并且还是可以一枚一枚列举出来的,建议使用枚举类型。
例如:颜色、四季、星期等都可以使用枚举类型。
采用枚举的方式改造程序:
public class EnumTest02 {
public static void main(String[] args) {
Result r = divide(10, 2);
System.out.println(r == Result.SUCCESS ? "计算成功" : "计算失败");
}
/**
* 计算两个int类型数据的商。
* @param a int数据
* @param b int数据
* @return Result.SUCCESS表示成功,Result.FAIL表示失败!
*/
public static Result divide(int a, int b){
try {
int c = a / b;
return Result.SUCCESS;
} catch (Exception e){
return Result.FAIL;
}
}
}
// 枚举:一枚一枚可以列举出来的,才建议使用枚举类型。
// 枚举编译之后也是生成class文件。
// 枚举也是一种引用数据类型。
// 枚举中的每一个值可以看做是常量。
enum Result{
// SUCCESS 是枚举Result类型中的一个值
// FAIL 是枚举Result类型中的一个值
// 枚举中的每一个值,可以看做是“常量”
SUCCESS, FAIL
}
例:
public enum Season {
/*
春夏秋冬
*/
SPRING, SUMMER, AUTUMN, WINTER
}
public class SwitchTest {
public static void main(String[] args) {
// switch语句支持枚举类型
// switch也支持String、int
// 低版本的JDK,只支持int
// 高版本的JDK,支持int、String、枚举。
// byte short char也可以,因为存在自动类型转换。
switch (Season.SPRING) {
// 必须省略Season.
case SPRING:
System.out.println("春天");
break;
case SUMMER:
System.out.println("夏天");
break;
case AUTUMN:
System.out.println("秋天");
break;
case WINTER:
System.out.println("冬天");
break;
}
}
}
异常
异常描述
1、什么是异常,java提供异常处理机制有什么用?
以下程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常
java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常情况,java把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对程序进行修改,让程序更加的健壮。
什么是异常:程序执行过程中的不正常情况。
异常的作用:增强程序的健壮性。
2、以下程序执行控制台出现了:
Exception in thread “main” java.lang.ArithmeticException: / by zero
at com.bjpowernode.javase.exception.ExceptionTest01.main(ExceptionTest01.java:14)
这个信息被我们称为:异常信息。这个信息是JVM打印的。
public class ExceptionTest01 {
public static void main(String[] args) {
int a = 10;
int b = 0;
// 实际上JVM在执行到此处的时候,会new异常对象:new ArithmeticException("/ by zero");
// 并且JVM将new的异常对象抛出,打印输出信息到控制台了。
int c = a / b;
System.out.println(a + "/" + b + "=" + c);
// 此处运行也会创建一个:ArithmeticException类型的异常对象。
//System.out.println(100 / 0);
// 我观察到异常信息之后,对程序进行修改,更加健壮。
/*
int a = 10;
int b = 2;
if(b == 0) {
System.out.println("除数不能为0");
return;
}
// 程序执行到此处表示除数一定不是0
int c = a / b;
System.out.println(a + "/" + b + "=" + c);
*/
}
}
java语言中异常以什么形式存在
1、异常在java中以类的形式存在,每一个异常类都可以创建异常对象。
2、异常对应的现实生活中是怎样的?
火灾(异常类):
2008年8月8日,小明家着火了(异常对象)
2008年9月8日,小刚家着火了(异常对象)
2008年9月8日,小红家着火了(异常对象)
类是:模板。
对象是:实际存在的个体。
钱包丢了(异常类):
2008年1月8日,小明的钱包丢了(异常对象)
2008年1月9日,小芳的钱包丢了(异常对象)
…
public class ExceptionTest02 {
public static void main(String[] args) {
// 通过“异常类”实例化“异常对象”
NumberFormatException nfe = new NumberFormatException("数字格式化异常!");
// java.lang.NumberFormatException: 数字格式化异常!
System.out.println(nfe);
// 通过“异常类”创建“异常对象”
NullPointerException npe = new NullPointerException("空指针异常发生了!");
//java.lang.NullPointerException: 空指针异常发生了!
System.out.println(npe);
}
}
java的异常处理机制
异常的继承结构
Throwable:不管是错误,还是异常,都是可抛出的。
Error:所有的错误只要发生,Java程序只有一个结果,那就是终止程序的执行。退出JVM,错误是不能处理的。
RuntimeException:所有的RuntimeException及子类都属于运行时异常。运行时异常在编写程序阶段,可以选择处理,也可以不处理。运行时异常发生概率较低。运行时异常还有另外一些名字:未受检异常,或者非受控异常
ExceptionSubClass(Exception的直接子类):所有Exception的直接子类,都叫做编译时异常。编译时异常是在编译阶段发生的吗?不是。编译时异常是表示必须在编写程序的时候预先对这种异常进行处理,如果不处理编译器报错。编译时异常发生概率较高。编译时异常又被称为受检异常,或者受控异常。
继承结构:
Object
Object下有Throwable(可抛出的)
Throwable下有两个分支:Error(不可处理,直接退出JVM)和Exception(可处理的)
Exception下有两个分支:
Exception的直接子类:编译时异常(要求程序员在编写程序阶段必须预先对这些异常进行处理,如果不处理编译器报错,因此得名编译时异常。)
RuntimeException:运行时异常(在编写程序阶段程序员可以预先处理,也可以不管,都行。)
编译时异常因什么而得名
编译时异常和运行时异常,都是发生在运行阶段。编译阶段异常是不会发生的。
编译时异常因什么而得名?
因为编译时异常必须在编译(编写程序)阶段预先处理,如果不处理编译器会报错,因此得名。
所有异常都是在运行阶段发生的。因为只有程序运行阶段才可以new对象。因为异常的发生就是new异常对象。
编译时异常和运行时异常的区别
编译时异常一般发生的概率比较高。
举个例子:
你看到外面下雨了,倾盆大雨的。
你出门之前会预料到:如果不打伞,我可能会生病(生病是一种异常)。
而且这个异常发生的概率很高,所以我们出门之前要拿一把伞。
“拿一把伞”就是对“生病异常”发生之前的一种处理方式。
运行时异常一般发生的概率比较低。
举个例子:
小明走在大街上,可能会被天上的飞机轮子砸到。
被飞机轮子砸到也算一种异常。
但是这种异常发生概率较低。
在出门之前你没必要提前对这种发生概率较低的异常进行预处理。
如果你预处理这种异常,你将活的很累。
假设你在出门之前,你把能够发生的异常都预先处理,你这个人会更加的安全,但是你这个人活得很累。
假设java中没有对异常进行划分,没有分为:编译时异常和运行时异常,所有的异常都需要在编写程序阶段对其进行预处理,将是怎样的效果呢?
首先,如果这样的话,程序肯定是绝对的安全的。但是程序员编写程序太累,代码到处都是处理异常的代码。
编译时异常还有其他名字:
受检异常:CheckedException
受控异常
运行时异常还有其他名字:
未受检异常:UnCheckedException
非受控异常
再次强调:所有异常都是发生在运行阶段的。
异常的两种处理方式
第一种方式:在方法声明的位置上,使用throws关键字,抛给上一级。
谁调用我,我就抛给谁。抛给上一级。
第二种方式:使用try…catch语句进行异常的捕捉。
这件事发生了,谁也不知道,因为我把它抓住了。
举个例子:
我是某集团的一个销售员,因为我的失误,导致公司损失了1000元,“损失1000元”这可以看作是一个异常发生了。我有两种处理方式:
第一种方式:我把这件事告诉我的领导【异常上抛】
第二种方式:我自己掏腰包把这个钱补上【异常的捕捉】
上抛后如果上级觉得处理不了,就会继续往上抛:张三 --> 李四 --> 王五 --> CEO
思考:
异常发生之后,如果我选择了上抛,抛给了我的调用者,调用者需要对这个异常继续处理,那么调用者处理这个异常同样有两种处理方式。
注意:Java中异常发生之后如果一直上抛,最终抛给了main方法,main方法继续向上抛,抛给了调用者JVM,JVM知道这个异常发生,只有一个结果:终止java程序的执行。
运行时异常编写程序时可以不处理
public class ExceptionTest03 {
public static void main(String[] args) {
/*
程序执行到此处发生了ArithmeticException异常,
底层new了一个ArithmeticException异常对象,
然后抛出了,由于是main方法调用了100 / 0,
所以这个异常ArithmeticException抛给了main方法,
main方法没有处理,将这个异常自动抛给了JVM。
JVM最终终止程序的执行。
ArithmeticException 继承 RuntimeException,属于运行时异常。
在编写程序阶段不需要对这种异常进行预先的处理。
*/
System.out.println(100 / 0);
// 这里的HelloWorld没有输出,没有执行。
System.out.println("Hello World!");
}
}
方法声明位置上使用throws
以下代码报错的原因是什么?
因为doSome()方法声明位置上使用了:throws ClassNotFoundException
而ClassNotFoundExcepiton是编译时异常。必须编写代码时处理,没有处理编译器报错。
public class ExceptionTest04 {
public static void main(String[] args) {
// main方法中调用doSome()方法
// 因为doSome()方法声明位置上有:throws ClassNotFoundException
// 我们在调用doSome()方法的时候必须对这种异常进行预先的处理。
// 如果不处理,编译器就报错。
//编译器报错信息: Unhandled exception: java.lang.ClassNotFoundException
doSome();
}
/**
* doSome方法在方法声明的位置上使用了:throws ClassNotFoundException
* 这个代码表示doSome()方法在执行过程中,有可能会出现ClassNotFoundException异常。
* 叫做类没找到异常。这个异常直接父类是:Exception,所以ClassNotFoundException属于编译时异常。
* @throws ClassNotFoundException
*/
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!!");
}
}
异常处理的原理
public class ExceptionTest05 {
// 第一种处理方式:在方法声明的位置上继续使用:throws,来完成异常的继续上抛。抛给调用者。
// 上抛类似于推卸责任。(继续把异常传递给调用者。)
/*
public static void main(String[] args) throws ClassNotFoundException {
doSome();
}
*/
// 第二种处理方式:try..catch进行捕捉。
// 捕捉等于把异常拦下了,异常真正的解决了。(调用者是不知道的。)
public static void main(String[] args) {
try {
doSome();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void doSome() throws ClassNotFoundException{
System.out.println("doSome!!!!");
}
}
异常捕捉和上报的联合使用
处理异常的第一种方式:
在方法声明的位置上使用throws关键字抛出,谁调用我这个方法,我就抛给谁。抛给调用者来处理。
这种处理异常的态度:上报。
处理异常的第二种方式:
使用try…catch语句对异常进行捕捉。
这个异常不会上报,自己把这个事儿处理了。
异常抛到此处为止,不再上抛了。
注意:
只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。
另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行。
try…catch捕捉异常之后,后续代码可以执行。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest06 {
// 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
// 异常处理机制的作用就是增强程序的健壮性。怎么能做到,异常发生了也不影响程序的执行。所以
// 一般main方法中的异常建议使用try..catch进行捕捉。main就不要继续上抛了。
/*
public static void main(String[] args) throws FileNotFoundException {
System.out.println("main begin");
m1();
System.out.println("main over");
}
*/
public static void main(String[] args) {
// 100 / 0这是算术异常,这个异常是运行时异常,你在编译阶段,可以处理,也可以不处理。编译器不管。
//System.out.println(100 / 0); // 不处理编译器也不管
// 你处理也可以。
/*
try {
System.out.println(100 / 0);
} catch(ArithmeticException e){
System.out.println("算术异常了!!!!");
}
*/
System.out.println("main begin");
try {
// try尝试
m1();
// 以上代码出现异常,直接进入catch语句块中执行。
System.out.println("hello world!");
} catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。
// 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。
// catch是捕捉异常之后走的分支。
// 在catch分支中干什么?处理异常。
System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");
System.out.println(e); //java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)
}
// try..catch把异常抓住之后,这里的代码会继续执行。
System.out.println("main over");
}
private static void m1() throws FileNotFoundException {
System.out.println("m1 begin");
m2();
// 以上代码出异常,这里是无法执行的。
System.out.println("m1 over");
}
// 抛别的不行,抛ClassCastException说明你还是没有对FileNotFoundException进行处理
//private static void m2() throws ClassCastException{
// 抛FileNotFoundException的父对象IOException,这样是可以的。因为IOException包括FileNotFoundException
//private static void m2() throws IOException {
// 这样也可以,因为Exception包括所有的异常。
//private static void m2() throws Exception{
// throws后面也可以写多个异常,可以使用逗号隔开。
//private static void m2() throws ClassCastException, FileNotFoundException{
private static void m2() throws FileNotFoundException {
System.out.println("m2 begin");
// 编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException
// 我们在这里调用m3()没有对异常进行预处理,所以编译报错。
// m3();
m3();
// 以上如果出现异常,这里是无法执行的!
System.out.println("m2 over");
}
private static void m3() throws FileNotFoundException {
// 调用SUN jdk中某个类的构造方法。
// 这个类还没有接触过,后期IO流的时候就知道了。
// 我们只是借助这个类学习一下异常处理机制。
// 创建一个输入流对象,该流指向一个文件。
/*
编译报错的原因是什么?
第一:这里调用了一个构造方法:FileInputStream(String name)
第二:这个构造方法的声明位置上有:throws FileNotFoundException
第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,
最终得知,FileNotFoundException是编译时异常。
错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。
*/
//new FileInputStream("D:\\course\\01-开课\\学习方法.txt");
// 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。
// 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。
new FileInputStream("D:\\course\\01-课\\学习方法.txt");
System.out.println("如果以上代码出异常,这里会执行吗??????????????????不会!!!");
}
}
一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
异常处理机制的作用就是增强程序的健壮性。怎么能做到异常发生了也不影响程序的执行。所以一般main方法中的异常建议使用try…catch进行捕捉,main就不要继续上抛了。
try catch深入
1、catch后面的小括号中的类型可以是具体的异常类型,也可以是该异常类型的父类型。
2、catch可以写多个。建议catch的时候,精确的一个一个处理。这样有利于程序的调试。
3、catch写多个的时候,从上到下,必须遵守从小到大。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest07 {
/*
public static void main(String[] args) throws Exception, FileNotFoundException, NullPointerException {
}
*/
/*public static void main(String[] args) throws Exception {
}*/
public static void main(String[] args) {
//编译报错
/*try {
FileInputStream fis = new FileInputStream("D:\\course\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(NullPointerException e) {
}*/
/*try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
System.out.println("以上出现异常,这里无法执行!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}
System.out.println("hello world!");*/
/*try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(IOException e) { // 多态:IOException e = new FileNotFoundException();
System.out.println("文件不存在!");
}*/
/*try {
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
} catch(Exception e) { // 多态:Exception e = new FileNotFoundException();
System.out.println("文件不存在!");
}*/
/*try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(Exception e) { //所有的异常都走这个分支。
System.out.println("文件不存在!");
}*/
/*try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
} catch(IOException e){
System.out.println("读文件报错了!");
}*/
// 编译报错。
/*
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
//读文件
fis.read();
} catch(IOException e){
System.out.println("读文件报错了!");
} catch(FileNotFoundException e) {
System.out.println("文件不存在!");
}
*/
// JDK8的新特性!
try {
//创建输入流
FileInputStream fis = new FileInputStream("D:\\curse\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
// 进行数学运算
System.out.println(100 / 0); // 这个异常是运行时异常,编写程序时可以处理,也可以不处理。
} catch(FileNotFoundException | ArithmeticException | NullPointerException e) {
System.out.println("文件不存在?数学异常?空指针异常?都有可能!");
}
}
}
上报和捕捉怎么选择
在以后的开发中,处理编译时异常,应该上报还是捕捉呢?怎么选?
如果希望调用者来处理,选择throws上报。
其他情况使用捕捉的方式。
异常对象的常用方法
异常对象有两个非常重要的方法:
获取异常简单的描述信息:
String msg = exception.getMessage();
打印异常追踪的堆栈信息:
exception.printStackTrace();
//一般都是使用这个
public class ExceptionTest08 {
public static void main(String[] args) {
// 这里只是为了测试getMessage()方法和printStackTrace()方法。
// 这里只是new了异常对象,但是没有将异常对象抛出。JVM会认为这是一个普通的java对象。
NullPointerException e = new NullPointerException("空指针异常fdsafdsafdsafds");
// 获取异常简单描述信息:这个信息实际上就是构造方法上面String参数。
String msg = e.getMessage(); //空指针异常fdsafdsafdsafds
System.out.println(msg);
// 打印异常堆栈信息
// java后台打印异常堆栈追踪信息的时候,采用了异步线程的方式打印的。
e.printStackTrace();
for(int i = 0; i < 1000; i++){
System.out.println("i = " + i);
}
System.out.println("Hello World!");
}
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest09 {
public static void main(String[] args) {
try {
m1();
} catch (FileNotFoundException e) {
// 获取异常的简单描述信息
String msg = e.getMessage();
System.out.println(msg); //C:\jetns-agent.jar (系统找不到指定的文件。)
//打印异常堆栈追踪信息!!!
//在实际的开发中,建议使用这个。养成好习惯!
// 这行代码要写上,不然出问题你也不知道!
//e.printStackTrace();
/*
java.io.FileNotFoundException: C:\jetns-agent.jar (系统找不到指定的文件。)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
at com.bjpowernode.javase.exception.ExceptionTest09.m3(ExceptionTest09.java:31)
at com.bjpowernode.javase.exception.ExceptionTest09.m2(ExceptionTest09.java:27)
at com.bjpowernode.javase.exception.ExceptionTest09.m1(ExceptionTest09.java:23)
at com.bjpowernode.javase.exception.ExceptionTest09.main(ExceptionTest09.java:14)
因为31行出问题导致了27行
27行出问题导致23行
23行出问题导致14行。
应该先查看31行的代码。31行是代码错误的根源。
*/
}
// 这里程序不耽误执行,很健壮。《服务器不会因为遇到异常而宕机。》
System.out.println("Hello World!");
}
private static void m1() throws FileNotFoundException {
m2();
}
private static void m2() throws FileNotFoundException {
m3();
}
private static void m3() throws FileNotFoundException {
new FileInputStream("C:\\jetns-agent.jar");
}
}
我们以后查看异常的追踪信息,应该怎么看,可以快速地调试程序呢?
异常追踪信息,从上往下一行一行看。
但是需要注意的是:SUN写的代码就不用看了(看包名就知道是自己的还是SUN的)。
主要的问题是出现在自己编写的代码上。
finally子句的使用
1、在finally子句中的代码是最后执行的,并且是一定会执行的,即使try语句块中的代码出现了异常。
finally子句必须和try一起出现,不能单独编写。
2、finally语句通常使用在哪些情况下呢?
通常在finally语句块中完成资源的释放/关闭。
因为finally中的代码比较有保障。
即使try语句块中的代码出现异常,finally中代码也会正常执行。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest10 {
public static void main(String[] args) {
FileInputStream fis = null; // 声明位置放到try外面。这样在finally中才能用。
try {
// 创建输入流对象
fis = new FileInputStream("D:\\course\\02-JavaSE\\document\\JavaSE进阶讲义\\JavaSE进阶-01-面向对象.pdf");
// 开始读文件....
String s = null;
// 这里一定会出现空指针异常!
s.toString();
System.out.println("hello world!");//不执行
// 流使用完需要关闭,因为流是占用资源的。
// 即使以上程序出现异常,流也必须要关闭!
// 放在这里有可能流关不了。
//fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
} catch(NullPointerException e) {
e.printStackTrace();
} finally {
System.out.println("hello 浩克!");
// 流的关闭放在这里比较保险。
// finally中的代码是一定会执行的。
// 即使try中出现了异常!
if (fis != null) { // 避免空指针异常!
try {
// close()方法有异常,采用捕捉的方式。
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("hello kitty!");
}
}
**再次强调:**放在finally语句块中的代码是一定会执行的!!!
public class ExceptionTest11 {
public static void main(String[] args) {
/*
try和finally,没有catch可以吗?可以。
try不能单独使用。
try finally可以联合使用。
以下代码的执行顺序:
先执行try...
再执行finally...
最后执行 return (return语句只要执行方法必然结束。)
*/
try {
System.out.println("try...");
return;
} finally {
// finally中的语句会执行。能执行到。
System.out.println("finally...");
}
// 这里不能写语句,因为这个代码是无法执行到的。
//System.out.println("Hello World!");
}
}
先执行try…
再执行finally…
最后执行 return(return语句只要执行方法必然结束)
退出JVM finally语句不执行
public class ExceptionTest12 {
public static void main(String[] args) {
try {
System.out.println("try...");
// 退出JVM
System.exit(0); // 退出JVM之后,finally语句中的代码就不执行了!
} finally {
System.out.println("finally...");
}
}
}
finally面试题
public class ExceptionTest13 {
public static void main(String[] args) {
int result = m();
System.out.println(result); //100
}
/*
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中海油一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)
*/
public static int m(){
int i = 100;
try {
// 这行代码出现在int i = 100;的下面,所以最终结果必须是返回100
// return语句还必须保证是最后执行的。一旦执行,整个方法结束。
return i;
} finally {
i++;
}
}
}
/*
反编译之后的效果
public static int m(){
int i = 100;
int j = i;
i++;
return j;
}
*/
java语法规则(有一些规则是不能破坏的,一旦这么说了,就必须这么做!):
java中有一条这样的规则:
方法体中的代码必须遵循自上而下顺序依次逐行执行(亘古不变的语法!)
java中还有一条语法规则:
return语句一旦执行,整个方法必须结束(亘古不变的语法!)
final finally finalize有什么区别
final 关键字
final修饰的类无法继承
final修饰的方法无法覆盖
final修饰的变量不能重新赋值
finally 关键字
和try一起联合使用
finally语句块中的代码是必须执行的
finalize 标识符
是一个Object类中的方法名
这个方法是由垃圾回收器GC负责调用的
java中自定义异常
1、SUN提供的JDK内置的异常肯定是不够用的。在实际的开发中,有很多业务,这些业务出现异常之后,JDK中都是没有的,和业务挂钩的。那么异常类我们可以自己定义吗?可以。
2、java中怎么自定义异常呢?
两步:
第一步:编写一个类继承Exception或者RuntimeException。
第二步:提供两个构造方法,一个无参数的,一个带有String参数的。
public class MyException extends Exception{ // 编译时异常
public MyException(){
}
public MyException(String s){
super(s);
}
}
/*
public class MyException extends RuntimeException{ // 运行时异常
}
*/
public class ExceptionTest15 {
public static void main(String[] args) {
// 创建异常对象(只new了异常对象,并没有手动抛出)
MyException e = new MyException("用户名不能为空!");
// 打印异常堆栈信息
e.printStackTrace();
// 获取异常简单描述信息
String msg = e.getMessage();
System.out.println(msg);
}
}
自定义异常在实际开发中的作用
异常最重要的案例,必须掌握:
/**
* 栈操作异常:自定义异常!
*/
public class MyStackOperationException extends Exception{ // 编译时异常!
public MyStackOperationException(){
}
public MyStackOperationException(String s){
super(s);
}
}
/*
编写程序,使用一维数组,模拟栈数据结构。
要求:
1、这个栈可以存储java中的任何引用类型的数据。
2、在栈中提供push方法模拟压栈。(栈满了,要有提示信息。)
3、在栈中提供pop方法模拟弹栈。(栈空了,也有有提示信息。)
4、编写测试程序,new栈对象,调用push pop方法来模拟压栈弹栈的动作。
5、假设栈的默认初始化容量是10.(请注意无参数构造方法的编写方式。)
*/
public class MyStack {
// 向栈当中存储元素,我们这里使用一维数组模拟。存到栈中,就表示存储到数组中。
// 因为数组是我们学习java的第一个容器。
// 为什么选择Object类型数组?因为这个栈可以存储java中的任何引用类型的数据
// new Animal()对象可以放进去,new Person()对象也可以放进去。因为Animal和Person的超级父类就是Object。
// 包括String也可以存储进去。因为String父类也是Object。
private Object[] elements;
// 栈帧,永远指向栈顶部元素
// 那么这个默认初始值应该是多少。注意:最初的栈是空的,一个元素都没有。
//private int index = 0; // 如果index采用0,表示栈帧指向了顶部元素的上方。
//private int index = -1; // 如果index采用-1,表示栈帧指向了顶部元素。
private int index;
/**
* 无参数构造方法。默认初始化栈容量10.
*/
public MyStack() {
// 一维数组动态初始化
// 默认初始化容量是10.
this.elements = new Object[10];
// 给index初始化
this.index = -1;
}
/**
* 压栈的方法
* @param obj 被压入的元素
*/
public void push(Object obj) throws MyStackOperationException {
if(index >= elements.length - 1){
// 改良之前
//System.out.println("压栈失败,栈已满!");
//return;
// 创建异常对象
//MyStackOperationException e = new MyStackOperationException("压栈失败,栈已满!");
// 手动将异常抛出去!
//throw e; //这里捕捉没有意义,自己new一个异常,自己捉,没有意义。栈已满这个信息你需要传递出去。
// 合并(手动抛出异常!)
throw new MyStackOperationException("压栈失败,栈已满!");
}
// 程序能够走到这里,说明栈没满
// 向栈中加1个元素,栈帧向上移动一个位置。
index++;
elements[index] = obj;
// 在声明一次:所有的System.out.println()方法执行时,如果输出引用的话,自动调用引用的toString()方法。
System.out.println("压栈" + obj + "元素成功,栈帧指向" + index);
}
/**
* 弹栈的方法,从数组中往外取元素。每取出一个元素,栈帧向下移动一位。
* @return
*/
public void pop() throws MyStackOperationException {
if(index < 0){
//System.out.println("弹栈失败,栈已空!");
//return;
throw new MyStackOperationException("弹栈失败,栈已空!");
}
// 程序能够执行到此处说明栈没有空。
System.out.print("弹栈" + elements[index] + "元素成功,");
// 栈帧向下移动一位。
index--;
System.out.println("栈帧指向" + index);
}
// set和get也许用不上,但是你必须写上,这是规矩。你使用IDEA生成就行了。
// 封装:第一步:属性私有化,第二步:对外提供set和get方法。
public Object[] getElements() {
return elements;
}
public void setElements(Object[] elements) {
this.elements = elements;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
public class ExceptionTest16 {
public static void main(String[] args) {
// 创建栈对象
MyStack stack = new MyStack();
// 压栈
try {
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
stack.push(new Object());
// 这里栈满了
stack.push(new Object());
} catch (MyStackOperationException e) {
// 输出异常的简单信息。
System.out.println(e.getMessage());
}
// 弹栈
try {
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
stack.pop();
// 弹栈失败
stack.pop();
} catch (MyStackOperationException e) {
System.out.println(e.getMessage());
}
}
}
异常与方法覆盖
重写(覆盖)之后的方法不能比重写之前的方法抛出更多(更宽泛)的异常,可以更少,甚至不抛出。
class Animal {
public void doSome(){
}
public void doOther() throws Exception{
}
}
class Cat extends Animal {
// 编译正常。运行时异常都可以抛
public void doSome() throws RuntimeException{
}
// 编译报错。
/*public void doSome() throws Exception{
}*/
// 编译正常。
/*public void doOther() {
}*/
// 编译正常。
/*public void doOther() throws Exception{
}*/
// 编译正常。
public void doOther() throws NullPointerException{
}
}
例子
例1:编写程序模拟用户注册:
1、程序开始执行时,提示用户输入“用户名”和“密码”信息。
2、输入信息之后,后台java程序模拟用户注册。
3、注册时用户名要求长度在[6-14]之间,小于或者大于都表示异常。
/**
* 自定义异常
*/
public class IllegalNameException extends Exception {
public IllegalNameException(){
}
public IllegalNameException(String s){
super(s);
}
}
/*
用户业务类,处理用户相关的业务:例如登录、注册等功能。
*/
public class UserService {
/**
* 用户注册
* @param username 用户名
* @param password 密码
* @throws IllegalNameException 当用户名为null,或者用户名长度小于6,或者长度大于14,会出现该异常!
*/
public void register(String username, String password) throws IllegalNameException {
/*
引用等于null的这个判断最好放到所有条件的最前面。
*/
/*if(username == null || username.length() < 6 || username.length() > 14){
}*/
/*
再分享一个经验:username == null 不如写成 null == username
"abc".equals(username) 比 username.equals("abc") 好。
*/
/*if(null == username || username.length() < 6 || username.length() > 14){
}*/
if(null == username || username.length() < 6 || username.length() > 14) {
/*System.out.println("用户名不合法,长度必须在[6-14]之间");
return;*/
throw new IllegalNameException("用户名不合法,长度必须在[6-14]之间");
}
// 程序能够执行到此处说明,用户名合法
System.out.println("注册成功,欢迎["+username+"]");
}
}
public class Test {
public static void main(String[] args) {
// 创建UserService对象
UserService userService = new UserService();
// 用户名和密码就不再从控制台接收了
try {
userService.register("jack", "123");
} catch (IllegalNameException e) {
//System.out.println(e.getMessage());
e.printStackTrace();
}
}
}
插曲笔记:
类在强制类型转换过程中,如果是类转换成接口类型,那么类和接口之间不需要存在继承关系也可以转换,java语法中允许。
什么是UML
UML(Unified Modeling Language)是一种统一建模语言。一种图标式语言(画图的)。
UML不是只有java中使用,只要是面向对象的编程语言,都有UML。
一般画UML图的都是软件架构师或者系统分析师,这些级别的人员使用的。软件设计人员使用UML。
在UML图中可以描述类和类之间的关系,程序执行的流程,对象的状态等。