这个文档用于记录我的java语法初级学习之旅。

本文档参考资料:

java开发环境

jdk(java development kit):java程序开发工具包,包含jre和开发人员使用的工具

jre(java runtime environment kit):java程序的运行环境,包含jvm和核心类库

java的源代码以.java为后缀,通过javac将源文件编译为字节码文件(.class),其中字节码文件的命名和main函数相同。然后通过java命令在jvm中将字节码文件编译、解释并运行。

javac编译后的字节码文件不会包含注释

IDEA的project结构

这里插一嘴为什么和参考资料中的学习顺序不太一样,因为24年五月份我已经学习过一点零碎的java基础语法,所以前面的数据类型和逻辑条件结构等基础知识这里不重复学习记录。

在Intelli IDEA中,project和module概念被提出

project是最顶级的结构单位,其次是module,一个项目中多个module相互依赖,module下有多个package,每个package管理其下属的多个java类文件

数组

概念:多个相同类型的数据按照一定顺序进行排列的集合,并使用用一个名字进行命名,通过编号对这些数据进行同意管理。

特点:一旦初始化完成,长度确定且不可改变。

声明数组

dataType[ ] arrayRefVar;或dataType arrayRefVar[ ];

含义:将arrayRefVar引用赋给一个类型为dataType的数组(这个数组此时还不存在)

for example: double[ ] myList; // 声明了item类型为double的名称为myList的数组

创建数组:

dataType[ ] arrayRefVar = new dataType[arraySize] (没有自定义初始值,默认赋值为0,0.0,null等)

dataType[ ] arrayRefVar = {item1, item2, …} (给元素赋上初始值)

如果提前声明了数组,则可以用下面的方式简化创建:arrayRefVar = new arrayType[]{item1,item2}(或者用arraySize来默认赋值)

数组的内存解析

jvm(java虚拟机)运行时的内存环境分为五个部分:程序计数器、虚拟机栈、本地方法栈、堆、方法区

对于数组的涉及的内存结构:

1.虚拟机栈:用于存放方法中声明的变量,如数组的引用。

2.堆:用于存放数组的实体,即:数组包含的的元素。

1

当jvm检测到数组时会将引用和数组的头元素地址(指针)放到栈的一个空间中,再把数组元素放到堆中,指针指向堆中数组的头元素。

当将另一个数组引用到栈中的引用时,jvm会将记录的头元素地址改为该数组的头元素的的地址,原地址删除,原数组如果没有其它引用将会被清除。

二维数组

二维数组是一维数组中嵌套了一个数组。

声明二维数组:

dataType[ ] [ ] arrayRefVar ;

创建二维数组:

dataType[ ][ ] arrayRefVar = {{ }, { }, { }};

dataType[ ][ ] arrayRefVar = new dataType[ ][ ];

面对对象

静态方法和非静态方法

静态方法(static)无法访问非静态变量,静态方法不是实例,是属于类,故可以直接被调用。

同理,静态变量也可以直接被调用,无需实例化。

可以简单理解为,静态的方法和变量是类的所用实例具有的公共不变特性,这种特性不会随着实例的不同而改变,故属于类,不需要实例化可以直接访问,然而非静态变量由于随着不同实例的改变而改变,故在没有经过实例化的情况下无法直接被静态方法调用。


下面是CS61B的算法与数据结构课,参考资料见标题

Java是一门对面对对象狂热的语言,所有的java文件都是封装成一个个类,可以运行的程序则是通过类中定义的主函数来存储。

类的初始化

这里的类是指”真正“的类,因为我觉得java有点过度使用类,有的程序不需要使用类但是也要封装在类的方法里执行,比如我仅仅写一个打印int a = 3;的程序,这就不算是真正的类。

这里类的初始化就是针对真正的类对其成员变量的声明的构造方法,本质上就是一个特别的方法。

public DLlist(){
this.size = 0;
// 哨兵模式
this.head = new DLIntNode(0);
this.tail = new DLIntNode(0);
}

这里就是对class DLlist进行初始化,初始化类DLlist的三个成员变量的值。

然而这三个伴随变量在完整的程序初期就已经提前声明了

public class DLlist{ 
int size;
DLIntNode head;
DLIntNode tail;
public DLlist(){
this.size = 0;
// 哨兵模式
this.head = new DLIntNode(0);
this.tail = new DLIntNode(0);
}
}

如果这里的this没有加上去,那么这个初始化就没有进行,这个size,head, tail就相当于局部变量,在外部始终是null(未赋值)的状态。

这里有一个问题,如果我将在外部直接对成员变量进行了初始化赋值,这样可行吗。

答案是可以,甚至public DLlist(){}可以直接抛弃。

但是为什么还要使用这个构造方法呢,因为有的时候类的定义需要从外部传入参数、继承需要改写构造方法等。

链表

单向链表(SLlist)

public class IntNode {
public int item;
public IntNode rest;

public IntNode(int x, IntNode r){
this.item = x;
this.rest = r;
}
}

这里定义了一个节点IntNode,节点由两部分组成,一部分是元素item,另一部分是指针。

public class SLlist {
private int size = 0;
private IntNode sentinal = new IntNode(0, null);
public SLlist(){
size = 0;
sentinal = new IntNode(0, null);
}
public void addFirst(int x){
sentinal.rest = new IntNode(x, sentinal.rest);
size += 1;
}
public void addLast(int x){
size += 1;
IntNode p = sentinal;
while(p.rest != null){
p = p.rest;
}
p.rest = new IntNode(x, null);
}

这里定义了一个名为SLlist的单向链表类,SLlist由若干个节点IntNode组成,同时SLlist默认包含一个元素为0的哨兵节点sentinal。

这里着重解释一下引用(指针)

当我想要添加一个新元素在链表的末尾时,表面上看,我将新元素构成的节点“赋值”给了末尾的指针(引用),如果这样理解,对于单向链表的基础api确实没有问题,但是这其实完全误解指针的底层原理,这将导致无法理解

public void printList() {
IntNode p = sentinal;
int i = 1;
if (i != 1) {
System.out.println(p.item);
}
i += 1;
p = p.rest;
}
public int getFirst(){
if (sentinal.rest == null){
throw new NoSuchElementException("List is empty");
}
else{
return sentinal.rest.item;
}
}
public int getLast() {
if (sentinal.rest == null) {
throw new NoSuchElementException("List is empty");
} else {
IntNode p = sentinal.rest;
while (p.rest != null) {
p = p.rest;
}
return p.item;
}
}

这几个高级api,包括后面的双向链表。

实际上,java的内存管理分为三类:标识符、内存地址、内存。当我声明一个sentinal节点时,它进行了两ih个步骤:

  1. 选取内存空间存储节点内容
  2. 将内存空间的地址返回给标识符

即:标识符指向内存地址,内存地址指向内存空间。标识符也就是指针(引用)

用getLast()方法举例:我新建了一个p标识符指向sentinal标识符的rest元素,但是标识符不会互相指向,这里实际上指向了sentinal对应的节点的rest部分的内存空间地址。换句话说现在同一个内存空间(地址)对应了两个标识符(也就是节点名)。然后如果这个rest的rest部分非null,就再将p指针指向rest.rest.rest,依此类推。

这样就纠正了赋值的错误理解,赋值不是把值赋给新变量,而是将新变量名指向原来的值,新变量名也就变成了指针,也叫做引用。

有了这个基础,双向链表就比较好理解了。

双向链表(DLlist)

首先定义DLIntNode(双向链表节点)

// 双向链表节点
public class DLIntNode {
public DLIntNode pre;
public DLIntNode next;
public int value;
public DLIntNode(int value) {
pre = null;
next = null;
this.value = value;
}
}

// 双向链表
public class DLlist {
private int size;
private DLIntNode head;
private DLIntNode tail;
public DLlist(){
this.size = 0;
// 哨兵模式
this.head = new DLIntNode(0);
this.tail = new DLIntNode(0);
}
public boolean isEmpty(){
return size == 0;
}
public int getSize(){
return size;
}
public void print(){
DLIntNode temp = head.next;
while(temp != null){
System.out.println(temp.value);
temp = temp.next;
}
}
public void addFirst(int value){
DLIntNode item = new DLIntNode(value);
if(head.next == null){
head.next = item;
}
else{
head.next.pre = item;
item.next = head.next;
head.next = item;
item.pre = head;
}
size ++;
}

这里主要注意addFirst()方法中的三个节点互相指向(一共出现了四个指针四次指向)。

异常(Throwable类)

java.lang.Throwable类是异常的根父类

Throwable的两个方法

  • public void printStatckTrace() : 打印异常的详细信息
  • public String getMessage() : 获取发生异常的原因(新建异常类时输入的字符串)

Throwable的两个子类

  • java.lang.Error : java虚拟机无法解决的严重问题,不编写针对性代码处理
  • java.lang.Exception : 编程错误或外在因素导致的问题,需要编写针对性代码进行异常处理使得程序继续运行

Error举例 :

  • java.StatckOverflowError : 栈溢出
  • java.OutOfMemoryError : 堆空间溢出

Exception分类 :

  • 编译时异常(受检异常) : 执行javac.exe命令时出现的异常
  • 运行时异常(非受检异常) : 执行java.exe命令时出现的异常(Error也可以视为非受检异常)

Exception举例 :

  • 非受检异常 : ArrayIndexOutOfBoundsException, NullPointerException, ClassCastException, NumberFormatException, InputMismatchException, ArithmeticExcption
  • 受检异常 : ClassNotFoundException FileNotFoundException IOException (读取写入异常)

异常处理

try{}catch{}finally{}和throw的作用 :

  • 对于设计者不希望出现的情况通过异常处理的方式解决掉这种情况
  • 对于受检异常使得正常编译,比如IOException
  • 使得异常后续的代码不受影响继续执行

try-catch-finally

catch和finally二选一

注意对于多个catch必须先写子类的Exception再写父类

finally主要是避免catch中出现了异常导致程序不正常中断
注意finally中的内容会在try和catch的return之前执行,也就是强制执行;但是如果使用System.exit()语句会强制退出中断进程,finally也就不会执行了

什么样的情况必须使用finally

— 比如IO文件流中出现异常,无论是否处理异常,都必须继续执行关闭资源的操作,否则会导致内存泄露

throw

操作方法 :

  • 在方法名后加上throws exceptions_name。

注意事项

  • 其它方法调用这个方法的时候如果没有使用tcf语句处理掉Exception,则必须也在方法名后加上throws语句。
    换句话说,这个语句就像狼来了,它不考虑实际发不发生exception,统一默认 会发生exception
  • 当我在父类使用了throws语句 抛出非受检异常 时,必须在子类中的重构的方法中也使用throws语句,且抛出的异常必须是父类方法的异常或者异常的子类
    原因 : 对于多态性(静态类型和动态类型),调用重构的方法时如果子类的异常没有抛出或者不是父类的子集,那么编译器将会在解析静态类型(父类)时认为这个方法错误
  • 当父类没有throws, 子类也 不能抛出受检异常 ,原因同上

tcf 和 throws的出现情况

  • 资源必须被执行后续操作,使用finally
  • 父子类或者调用方法有一环没有使用throws,其余均只能使用tcf
  • 对于递进的方法,每个方法均throws,整体用tcf进行打包,这样既避免整体程序中断,又避免出现异常后的方法被执行而报错或混乱

手动抛出异常

前面讲的tcf和throws(受检和非受检异常)都是java体系内的错误,但是有时候程序运行没问题但是我不希望这种情况出现,这时就需要手动抛出异常

throw 和 throws 的区别

throw是手动抛出异常
throws则是异常处理

自定义异常

如何自定义异常

  • 自定义一个类且继承异常父类,通常为Exception/RuntimeException
  • 提供一个全局变量, 声明为:static final long serialVersionUID
  • 重构或定义自己的方法

自定义异常的意义

可以自定义异常的名称,通过名称辅助理解异常出现的原因

复习内容

131-异常处理-第09章复习与企业真题_哔哩哔哩_bilibili

Project推荐

130-异常处理-项目三:优尚开发团队人员调度系统的介绍、开发与测试_哔哩哔哩_bilibili

一个命令行的数据处理调度系统,不涉及IO操作,单次执行程序单次处理数据不保存。

完成项目可以升级:

  1. 增加IO操作,永久保存
  2. 增加GUI,将命令行指令改为按键监听触发api
  3. 增加云端操作,进行项目上线

多线程

多线程调度方式

  • 分时调度
  • 抢占式调度

并行

多个进程在多个cpu上同时运行

并发

多个进程依次在一个cpu上进行且每次只执行进程的一部分

多线程创建方式

继承Thread类

  • 创建Thread的子类
  • 重写run()方法,将线程执行操作声明在方法中
  • 调用对象的start()方法来执行run()方法执行线程

实现Runnable接口

  • 创建实现Runnable接口的类,然后实现run()方法并将要实现的功能放在其中
  • 新建Thread()实例并传入新建的上述类的实例并调用Thread的run方法

start()方法的作用:1.新建并启动线程 2.调用当前线程的run()

同一个实例的start只能被调用一次不能重复调用,如果需要创建两个线程,需要新建一个新的实例再调用
** 对于runnable接口的实现方式,可以多次调用run方法,并且每次调用共享私有变量

两种方法的联系:

1.由于Thread类实现了Runnable接口,所以方法二比方法一更接近本源,Thread这种实现方式被称为代理模式

2.但是由于Thread的start方法中实现了检测是否在构造时传入了runnable接口的逻辑判断,如果传入了接口优先使用接口的run方法,否则执行Thread的run方法
所以这个方法二还是使用了方法一

Thread类的构造器

  • 无传入变量,构造默认Thread实例
  • 传入String name,构造一个指定名称name的Thread
  • 传入Runnable target, 构造实现了Runnable接口的类的Thread并优先执行这个类的run方法
  • 传入Runnable targe,String name, 在上一个构造器的基础上传入了Thread名称

Thread类的其它方法

  • setName() : 设置线程名
  • getName() : 获取线程名
  • sleep(long ms) : 使得线程休眠指定时间,会抛出异常,需要使用tryCatch处理异常
  • yield() : 静态方法,使得前一个线程释放CPU,使得下一个线程优先执行
  • join() : 使得当前执行的全部线程进入阻塞状态,执导执行join方法的实例的线程执行完毕,会抛出异常,使用tryCatch处理异常
  • isAlive() : 判断线程是否存活,返回boolean

Thread的线程优先级

  • getPriority() : 获取线程优先级
  • setPriority(int) : 设置线程优先级(1 - 10) 1:最低, 5:默认, 10:最高

线程的生命周期

jdk5之前 :
1.新建:start()
2.就绪:
3.运行:run()
4.阻塞:sleep(), join(), wait(), 失去同步锁, notify()/notifyAll(), suspend()(resume()消除suspend方法效果)
5.死亡:run()执行结束或者出现异常或者stop()