CS61B 21sp Lab6: Getting Started on Project 2

501 阅读7分钟

CS61B 21sp Lab6: Getting Started on Project 2

hello, 大家好, 我是青岩石, 今天是2023/11/22, 想要将CS61B中的Lab6给学完,

Lab6的学习对于CS61B大名鼎鼎的Proj-Gitlet 有着决定性的帮助.

Intro

在本轮Lab中, 我们学习的目标有:

  1. 知道如何使用命令行运行Java

  2. 学会如何在Java中使用文件和目录

  3. 如何将Java对象序列为一个文件, 并在之后读取它们.

之前, 我们不能保存程序的状态. 当程序结束的时候, 程序使用的变量也就消除了. 但是现在我们可以记录程序的状态, 当程序再次运行的时候, 记录之前的记录.
从字面意思来看, 保存程序执行的本地数据.

有两种方法:

  1. 将纯文本写入进文件中
  2. 对象序列到文件中 (解决问题3.1: 对象序列到文件中的作用是将程序执行的数据作为本地数据进行保存, 增加状态)

Persistence | 持久性

文件系统 -> 持久性 -> git

这个lab的主要目的是为了proj2-gitlet(一个简易版本的git)

git特性: 建立了一个本地环境(增加了状态) -> 保存 + 使用

持久性: 可以持久保存内容

计算机实现持久性的方法: 文件系统

信息 存储到 hard drive 中

Java 和 编译

运行Java代码的两种方法

  1. GUI: 在Intellij中点击Run
  2. 命令行: 在终端上, 编译和运行Java代码
    1. javac: java(java代码) -> class(字节码)
    2. java: java程序(不是java代码, 应该是应该执行Java的程序) 编译成机器代码, 然后硬件执行它
$ javac -version 
$ java -version

javac + java -> 执行java文件中的main函数(参数)

其中文件一旦被限制在包之下, 则只能通过包名索引到文件之下

可以想象, 有一个变量记录了包, 包中有一个变量记录了该文件. 但是没有直接记录该文件的变量

java 文件名  参数

java 类名 == 选择java类中的main函数

java 类名 参数 == 将参数传入到该main函数中

空格 == 分割字符串为 独立的参数 "" == 将空格纳入其中, 从第一个" 到 第二个" 表示为一个字符串

小tips: *.java 可以代表目录下的所有java文件

$ javac *.java

Make

使用命令行执行gitlet -> 测试git函数的正确性也是通过命令行测试

举例: git status

  1. add
  2. status -> 保存了add的内容

测试方法:

  1. 是否正确的记住了程序早期执行中发生的情况
  2. gitlet的输出复杂 -> 解析后的输出是否正确

make的方法:

  1. make 编译代码
  2. make check -- 运行测试套件 (-> 测试函数是否正确)
  3. make clean -- 删除所有的class文件

Java中的文件和目录

System.getProperty("user.dir") 返回 Java当前目录

Java当前目录 == 执行Java程序时所在的目录

相对文件目录(.): 将当前文件作为索引

绝对文件路径(~): 将根目录作为索引

父级目录: ..

切换当前路径的方法: cd 新路径 (当前路径 -> 新路径)

java程序的文件操作和目录操作

此时教导我们的是第二个问题, 如何使用Java操作文件

Java中有一个File类, 将Java当前目录视作为当前目录, 构造文件或者文件夹.

File类的函数:

File f = new File("dummy.txt"); // 在当前目录下引用了一个新文件
f.createFile();  // 在当前路径下, 创建引用的新文件
f.exists();  // 当前路径是否存在该文件

在文件中写入内容的方法过于复杂 -> 制定了Utils.writeContents(f, "内容"); -> 将内容插入到文件中

Utils.writeContents(f, "hello 总是写个内容就好了, 它会插入到f引用的文件中");
  1. File构造函数接收的参数为文件名(如"*.java") -> 引用的是文件 -> 创1建文件的方法为createFile()
  2. 如果参数名不带后缀表示为文件夹(如"hi") -> 引用的是文件夹 -> 创建文件夹的方法为mkdir()

Serializable 序列化

这个主题讲解的则是第三个问题, 如何将对象序列为一个文件

序列化: 对象 -> 字节码存储在文件中 -> 可以使用一些方法重新格式化字节码, 再使用

实现序列化的方法: java.io.Serializable接口 -- 表示可以作为序列化的对象

import java.io.Serializable;

public class Model implements Serializable {
}

对象 序列化到 文件中:

writeObject(outFile, m);

文件 将对象反序列化 传入到对象中:

Model m;
File inFile = new File(saveFileName);

// Deserializing the Model object
m = readObject(inFile, Model.class);

Exercise

文件:

  1. Main.java

使用方法: java capers.Main args

  1. CapersRepository.java == 处理类之间的协调

  2. Dog.java == 代表一只有名字, 品种 和 年龄的狗

  3. Utils.java == 文件操作和序列化的使用函数

Main

story [text] : 添加到.capers目录下的story文件中, 打印当前的内部

dog [name] [breed] [age]: 创建带有指定参数的狗

birthday [name]: 持续增加狗的年龄

持久性数据 应该存入到 .capers 目录中 -> 我们需要在当前目录中创建一个.capers文件夹

为什么要加.前缀 -> 因为加了.前缀表示该文件夹隐藏 -> 不可以被查询

Useful Util Functions

  1. static void writeContents(File file, Object...contents)

将contents(数组) 写入 file中

  1. static String readContentsAsString(File file)

以字符串 读入 file

  1. sttic byte[] readContens(File file)

以字节数组的形式写入文件

  1. static void writeObject(File file, Serializabel obj)

将可虚列化对象写入文件

  1. static T readObject(File file, Class expectedClass)

从文件中读入可序列对象

可以使用类.class获取文件中的class对象

即:

readObject(file, Dog.class)    

独缺的就是文件内的Dog对象

  1. static File join(String first, Stirng ... others)

从前到后链接为一个新的路径

完成的顺序

  1. 填写CAPERS.FOLDER 和 DOG.FOLDER, 然后编写setUpPersistence

这两的意思是在当前目录下建立一个对.Caper文件夹的引用, 然后在.Caper文件夹下建立一个对.Dog文件夹的引用

然后则是编写函数, 建造这两个文件夹

// 建立 .capers文件夹
static final File CAPERS_FOLDER = new File(".capers");
    
// 建立 .Dog文件夹, 因为Dog在.capers之下, 所有使用join函数拼接目录
static final File DOG_FOLDER = join(".capers", "dogs");

// setup函数: 构造文件夹    
public static void setupPersistence() {
    CAPERS_FOLDER.mkdir();
    Dog.DOG_FOLDER.mkdir();
}    
  1. 第二个任务则是单纯的在Main函数中增加对于创建文件夹的调用

  2. 第三个任务是在CaperRepository中编写writeStory方法

将字符串写入.Caper中的story文件中

// 建立对于story文件的引用
File story = join(CAPERS_FOLDER, "story.txt");

// 这里有两种情况:  1. story文件不存在, 不用修改字符串 + 操作文件前, 会构造一个文件
// 2. story文件存在, 字符串为原本文件内的字符串 和 新加入的字符串
if (story.exists()) {
    text = readContentsAsString(story) + "\n" + text;
}

// 将字符串写入文件内
writeContents(story, text);

// 打印文件
System.out.println(text);
  1. 编写Dog 函数

saveDog 将Dog对象存入到文件中 | 狗的名字是独一无二的 -> 建立一个新的Dog文件

public void saveDog() {
    // TODO (hint: don't forget dog names are unique) -> create new File in dog dir
    // 建立Dog文件
    File dog = join(DOG_FOLDER, name);
    // 将Dog对象写入文件中
    writeObject(dog, this);
}

fromFile 从文件中提取Dog

public static Dog fromFile(String name) {
    // TODO (hint: look at the Utils file)
    // 引用Dog文件
    File dog = join(DOG_FOLDER, name);
    // 返回Dog对象
    return readObject(dog, Dog.class);
}
  1. 编写makeDog 和 celebrateBirthday 函数

public static void makeDog(String name, String breed, int age) {
    // TODO
    // 构造Dog实例
    Dog newDog = new Dog(name, breed, age);
    // Dog实例存入DogName文件中
    newDog.saveDog();
    // 打印Dog实例
    System.out.println(newDog);
}
public static void celebrateBirthday(String name) {
    // TODO

    // 从文件中提取出Dog实例
    Dog dogFromFile = Dog.fromFile(name);

    // 改变Dog实例 -> age + 1
    dogFromFile.haveBirthday();
    
    // 再将Dog实例存入文件中
    dogFromFile.saveDog();
}

建立对于Main方法中, 参数的不同, 执行什么操作

case "dog":
    validateNumArgs("dog", args, 4);
    // TODO: make a dog
    CapersRepository.makeDog(args[1], args[2], Integer.parseInt(args[3]));
    break;
case "birthday":
    validateNumArgs("birthday", args, 2);
    // TODO: celebrate this dog's birthday
    CapersRepository.celebrateBirthday(args[1]);
    break;