App 1 week ago
parent
commit
93fa607132
  1. 3
      project/.vscode/settings.json
  2. 917
      project/OOP_封装_继承_多态.md
  3. 20
      project/src/main/java/com/movieratings/model/Movie.java
  4. BIN
      project/target/classes/com/movieratings/model/Movie.class
  5. 625
      project/项目流程文档.md
  6. 72
      w5/README.md
  7. BIN
      w5/shape.png
  8. BIN
      w5/shape/Circle.class
  9. 6
      w5/shape/Circle.java
  10. BIN
      w5/shape/Main.class
  11. 8
      w5/shape/Main.java
  12. BIN
      w5/shape/Rectangle.class
  13. 6
      w5/shape/Rectangle.java
  14. BIN
      w5/shape/Shape.class
  15. 3
      w5/shape/Shape.java
  16. BIN
      w5/vehicle.png
  17. BIN
      w5/vehicle/Bike.class
  18. 6
      w5/vehicle/Bike.java
  19. BIN
      w5/vehicle/Car.class
  20. 6
      w5/vehicle/Car.java
  21. BIN
      w5/vehicle/Main.class
  22. 10
      w5/vehicle/Main.java
  23. BIN
      w5/vehicle/Truck.class
  24. 6
      w5/vehicle/Truck.java
  25. BIN
      w5/vehicle/Vehicle.class
  26. 3
      w5/vehicle/Vehicle.java
  27. BIN
      w6/Animal.class
  28. 3
      w6/Animal.java
  29. BIN
      w6/Animal.png
  30. BIN
      w6/Cat.class
  31. 6
      w6/Cat.java
  32. BIN
      w6/Dog.class
  33. 11
      w6/Dog.java
  34. BIN
      w6/Main.class
  35. 11
      w6/Main.java
  36. 75
      w6/README.md
  37. BIN
      w6/Swimmable.class
  38. 3
      w6/Swimmable.java

3
project/.vscode/settings.json

@ -0,0 +1,3 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

917
project/OOP_封装_继承_多态.md

@ -0,0 +1,917 @@
# 封装、继承、多态(Java示例)
本文档基于当前项目中的 Java 源代码,整理了封装、继承、多态三个面向对象核心概念的说明和示例代码。
## 1. 说明
### 1.1 封装(Encapsulation)
封装是把类的数据(属性)和对数据的操作(方法)放在一起。通过把字段设为 `private`,再提供 `public` getter/setter 方法,让外部不能直接访问内部数据,从而保护数据、隐藏实现。
项目中典型示例:
- `src/main/java/com/movieratings/model/Movie.java`
- `src/main/java/com/movieratings/model/DirectorStats.java`
### 1.2 继承(Inheritance)
继承让一个类获得另一个类或接口的特性。Java 中使用 `extends` 表示类继承类,使用 `implements` 表示类实现接口。接口也可以继承其他接口。
项目中典型示例:
- `src/main/java/com/movieratings/repository/MovieRepository.java`
- `src/main/java/com/movieratings/DataInitializer.java`
- `src/main/java/com/movieratings/model/Movie.java`
- `src/main/java/com/movieratings/model/DirectorStats.java`
### 1.3 多态(Polymorphism)
多态是指同一个引用类型,运行时可以指向不同的对象,并执行不同的实现。通常表现为接口类型引用指向实现类对象,或者子类重写父类方法。
项目中典型示例:
- `DataInitializer` 实现了 `CommandLineRunner`,Spring 会以接口类型调用它
- `MovieRepository` 继承自 `JpaRepository`,可以把它当作 `JpaRepository` 使用
- `Movie` 重写了 `toString()` 方法,展示了动态绑定
## 2. 相关代码
### 2.1 `Movie.java` - 封装与多态示例
文件路径:`src/main/java/com/movieratings/model/Movie.java`
```java
package com.movieratings.model;
import javax.persistence.*;
import java.io.Serializable;
/**
* 电影数据实体类
*/
@Entity
@Table(name = "movies")
public class Movie implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title; // 标题
private double rating; // 评分
private int releaseYear; // 年份
private int rank; // 排名
private String quote; // 简评/台词
private String director; // 导演
private int reviewCount; // 评价人数
private String country; // 国家/地区
private double boxOffice; // 票房 (模拟/演示)
private String type; // 作品类型 (电影、电视剧、纪录片等)
private String posterUrl; // 海报图片链接
public Movie() {}
public Movie(String title, double rating, int releaseYear, int rank, String quote, String director, int reviewCount, String country, double boxOffice, String type, String posterUrl) {
this.title = title;
this.rating = rating;
this.releaseYear = releaseYear;
this.rank = rank;
this.quote = quote;
this.director = director;
this.reviewCount = reviewCount;
this.country = country;
this.boxOffice = boxOffice;
this.type = type;
this.posterUrl = posterUrl;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public double getBoxOffice() { return boxOffice; }
public void setBoxOffice(double boxOffice) { this.boxOffice = boxOffice; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public double getRating() { return rating; }
public void setRating(double rating) { this.rating = rating; }
public int getReleaseYear() { return releaseYear; }
public void setReleaseYear(int releaseYear) { this.releaseYear = releaseYear; }
public int getRank() { return rank; }
public void setRank(int rank) { this.rank = rank; }
public String getQuote() { return quote; }
public void setQuote(String quote) { this.quote = quote; }
public String getDirector() { return director; }
public void setDirector(String director) { this.director = director; }
public int getReviewCount() { return reviewCount; }
public void setReviewCount(int reviewCount) { this.reviewCount = reviewCount; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getPosterUrl() { return posterUrl; }
public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; }
@Override
public String toString() {
return "Movie{" +
"id=" + id +
", title='" + title + '\'' +
", rating=" + rating +
", releaseYear=" + releaseYear +
", rank=" + rank +
", quote='" + quote + '\'' +
", director='" + director + '\'' +
", reviewCount=" + reviewCount +
", type='" + type + '\'' +
'}';
}
/**
* 重写 equals() — 两部电影 ID 相同则认为是同一部
* equals 和 hashCode 必须配对重写
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Movie other = (Movie) obj;
return id != null && id.equals(other.id);
}
/**
* 重写 hashCode() — 与 equals() 保持一致,基于 id 计算
*/
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}
```
#### 2.1.1 说明
- 所有字段都使用 `private`,这是封装的核心。
- 通过 `public getXxx()``public setXxx(...)` 提供访问和修改接口。
- `toString()` 方法被重写,这是多态的一种体现:运行时调用的是 `Movie` 的实现。
- **`equals()``hashCode()` 配对重写** — 这是 Java OOP 的重要规则:
- 如果两个对象 `equals()` 为 true,它们的 `hashCode()` 必须相等。
- 重写 `equals()` 时必须同时重写 `hashCode()`,否则在 `HashMap`、`HashSet` 等集合中会出错。
- `getClass() != obj.getClass()` 确保只比较同类型的对象。
### 2.2 `DirectorStats.java` - 封装示例
文件路径:`src/main/java/com/movieratings/model/DirectorStats.java`
```java
package com.movieratings.model;
import java.io.Serializable;
/**
* 导演作品统计 DTO
*/
public class DirectorStats implements Serializable {
private String director; // 导演姓名
private long totalWorks; // 作品总数
private String representativePoster; // 代表作品海报
private double averageRating; // 平均评分
private double totalBoxOffice; // 总票房
public DirectorStats(String director, long totalWorks, String representativePoster, double averageRating, double totalBoxOffice) {
this.director = director;
this.totalWorks = totalWorks;
this.representativePoster = representativePoster;
this.averageRating = averageRating;
this.totalBoxOffice = totalBoxOffice;
}
public String getDirector() { return director; }
public void setDirector(String director) { this.director = director; }
public long getTotalWorks() { return totalWorks; }
public void setTotalWorks(long totalWorks) { this.totalWorks = totalWorks; }
public String getRepresentativePoster() { return representativePoster; }
public void setRepresentativePoster(String representativePoster) { this.representativePoster = representativePoster; }
public double getAverageRating() { return averageRating; }
public void setAverageRating(double averageRating) { this.averageRating = averageRating; }
public double getTotalBoxOffice() { return totalBoxOffice; }
public void setTotalBoxOffice(double totalBoxOffice) { this.totalBoxOffice = totalBoxOffice; }
}
```
#### 2.2.1 说明
- 类字段同样设为 `private`
- 通过 getter/setter 访问属性,这是封装的标准写法。
### 2.3 `MovieRepository.java` - 继承示例
文件路径:`src/main/java/com/movieratings/repository/MovieRepository.java`
```java
package com.movieratings.repository;
import com.movieratings.model.Movie;
import com.movieratings.model.DirectorStats;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
/**
* 按导演统计作品数量排行榜,支持搜索、作品类型过滤和分页
*/
@Query("SELECT new com.movieratings.model.DirectorStats(m.director, COUNT(m), MAX(m.posterUrl), AVG(m.rating), SUM(m.boxOffice)) " +
"FROM Movie m " +
"WHERE (:name IS NULL OR m.director LIKE %:name%) " +
"AND (:type IS NULL OR m.type = :type) " +
"GROUP BY m.director " +
"ORDER BY COUNT(m) DESC")
Page<DirectorStats> findDirectorRankings(@Param("name") String name, @Param("type") String type, Pageable pageable);
/**
* 获取指定导演的作品列表
*/
List<Movie> findByDirector(String director);
/**
* 获取所有不同的作品类型
*/
@Query("SELECT DISTINCT m.type FROM Movie m WHERE m.type IS NOT NULL")
List<String> findAllTypes();
}
```
#### 2.3.1 说明
- `MovieRepository` 通过 `extends JpaRepository<Movie, Long>` 继承了 Spring Data JPA 提供的通用数据访问方法。
- 这就是继承的写法,子接口自动拥有父接口的行为。
### 2.5 `DataAnalyzer.java` - 内部类封装与方法封装
文件路径:`src/main/java/com/movieratings/analysis/DataAnalyzer.java`
```java
package com.movieratings.analysis;
import com.movieratings.model.Movie;
import org.apache.commons.math3.stat.correlation.PearsonsCorrelation;
import org.apache.commons.math3.stat.inference.TTest;
import java.util.*;
import java.util.stream.Collectors;
/**
* 数据分析类
*/
public class DataAnalyzer {
/**
* 年份与评分相关性分析 (Pearson 相关系数)
*/
public CorrelationResult analyzeYearRatingCorrelation(List<Movie> movies) {
double[] years = movies.stream().mapToDouble(Movie::getReleaseYear).toArray();
double[] ratings = movies.stream().mapToDouble(Movie::getRating).toArray();
PearsonsCorrelation correlation = new PearsonsCorrelation();
double coefficient = correlation.correlation(years, ratings);
TTest tTest = new TTest();
double pValue = tTest.tTest(years, ratings);
return new CorrelationResult(coefficient, pValue);
}
/**
* 导演作品统计结果类(内部静态类,体现封装)
*/
public static class DirectorStats {
private String name; // 导演姓名
private long count; // 作品数量
private double avgRating; // 平均评分
private double totalBoxOffice; // 总票房
public DirectorStats(String name, long count, double avgRating, double totalBoxOffice) {
this.name = name;
this.count = count;
this.avgRating = avgRating;
this.totalBoxOffice = totalBoxOffice;
}
public String getName() { return name; }
public long getCount() { return count; }
public double getAvgRating() { return avgRating; }
public double getTotalBoxOffice() { return totalBoxOffice; }
}
/**
* 导演作品数量排行榜 (前 20 位)
*/
public List<DirectorStats> getTopDirectors(List<Movie> movies, int topN) {
Map<String, List<Movie>> directorMap = movies.stream()
.filter(m -> m.getDirector() != null)
.collect(Collectors.groupingBy(Movie::getDirector));
return directorMap.entrySet().stream()
.map(entry -> {
String name = entry.getKey();
List<Movie> directorMovies = entry.getValue();
long count = directorMovies.size();
double avgRating = directorMovies.stream()
.mapToDouble(Movie::getRating).average().orElse(0.0);
double totalBoxOffice = directorMovies.stream()
.mapToDouble(m -> m.getBoxOffice()).sum();
return new DirectorStats(name, count, avgRating, totalBoxOffice);
})
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
.limit(topN)
.collect(Collectors.toList());
}
/**
* 相关性结果封装类(内部静态类)
*/
public static class CorrelationResult {
private double coefficient;
private double pValue;
public CorrelationResult(double coefficient, double pValue) {
this.coefficient = coefficient;
this.pValue = pValue;
}
public double getCoefficient() { return coefficient; }
public double getPValue() { return pValue; }
public String getSignificance() {
if (pValue < 0.01) return "极显著 (p < 0.01)";
if (pValue < 0.05) return "显著 (p < 0.05)";
return "不显著 (p >= 0.05)";
}
}
/**
* 统计评分基本信息
*/
public DoubleSummaryStatistics analyzeRatings(List<Movie> movies) {
return movies.stream()
.mapToDouble(Movie::getRating)
.summaryStatistics();
}
/**
* 按评分段统计
*/
public Map<String, Long> countMoviesByRatingRange(List<Movie> movies) {
return movies.stream()
.collect(Collectors.groupingBy(m -> {
double r = m.getRating();
if (r >= 9.5) return "9.5-10.0";
if (r >= 9.0) return "9.0-9.4";
if (r >= 8.5) return "8.5-8.9";
return "8.5以下";
}, Collectors.counting()));
}
/**
* 找出评价人数最多的前 N 部电影
*/
public List<Movie> findMostReviewed(List<Movie> movies, int n) {
return movies.stream()
.sorted((m1, m2) -> Integer.compare(m2.getReviewCount(), m1.getReviewCount()))
.limit(n)
.collect(Collectors.toList());
}
}
```
#### 2.5.1 说明
- **封装**:`DirectorStats` 和 `CorrelationResult``DataAnalyzer` 的**内部静态类**,字段为 `private`,通过 `public` getter 暴露数据,是封装的典型应用。
- **封装**:`getSignificance()` 方法把 p 值的判断逻辑封装在类内部,外部只需调用方法,不需要知道判断规则。
- **封装**:各个分析方法(`analyzeRatings`、`countMoviesByRatingRange` 等)将复杂的数据处理逻辑封装为单一方法调用。
- **多态**:`sorted((m1, m2) -> ...)` 使用了函数式接口 `Comparator`,这是多态在 Java 8+ 中的体现——接口引用指向 Lambda 表达式实现。
---
### 2.6 `MovieCrawler.java` - 常量封装与私有方法封装
文件路径:`src/main/java/com/movieratings/crawler/MovieCrawler.java`
```java
package com.movieratings.crawler;
import com.movieratings.model.Movie;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 电影数据爬虫类 - 抓取豆瓣 Top 250
*/
@Component
public class MovieCrawler {
// 封装:常量使用 private static final,外部无法修改
private static final String BASE_URL = "https://movie.douban.com/top250";
private static final String USER_AGENT = "Mozilla/5.0 ...";
// 公开方法:提供单一清晰的调用入口
public List<Movie> crawl(int limit) {
List<Movie> movies = new ArrayList<>();
int start = 0;
while (movies.size() < limit && start < 250) {
String url = BASE_URL + "?start=" + start + "&filter=";
try {
Document doc = Jsoup.connect(url).userAgent(USER_AGENT).get();
Elements items = doc.select(".item");
if (items.isEmpty()) break;
for (Element item : items) {
if (movies.size() >= limit) break;
try {
Movie movie = parseMovie(item); // 调用私有方法
movies.add(movie);
} catch (Exception e) {
System.err.println("解析失败: " + e.getMessage());
}
}
start += 25;
Thread.sleep(1000); // 控制请求频率
} catch (IOException | InterruptedException e) {
System.err.println("网络请求失败: " + e.getMessage());
break;
}
}
return movies;
}
// 封装:私有方法隐藏解析细节,外部只需调用 crawl()
private Movie parseMovie(Element item) {
Movie movie = new Movie();
movie.setRank(Integer.parseInt(item.select(".pic em").text()));
movie.setTitle(item.select(".title").first().text());
movie.setRating(Double.parseDouble(item.select(".rating_num").text()));
movie.setPosterUrl(item.select(".pic img").attr("src"));
movie.setType("电影");
String bdText = item.select(".bd p").first().text();
String[] parts = bdText.split("\n");
String infoLine = parts[0];
Pattern yearPattern = Pattern.compile("\\d{4}");
Matcher matcher = yearPattern.matcher(infoLine);
if (matcher.find()) {
movie.setReleaseYear(Integer.parseInt(matcher.group()));
}
if (infoLine.contains("导演: ")) {
int start = infoLine.indexOf("导演: ") + 4;
int end = infoLine.indexOf(" ", start);
if (end == -1) end = infoLine.length();
movie.setDirector(infoLine.substring(start, end).trim());
}
String[] infoParts = infoLine.split(" / ");
if (infoParts.length >= 3) {
movie.setCountry(infoParts[infoParts.length - 2].trim());
}
Element starDiv = item.selectFirst(".star");
if (starDiv != null) {
String starText = starDiv.text();
Pattern reviewPattern = Pattern.compile("([\\d,]+)人评价");
Matcher reviewMatcher = reviewPattern.matcher(starText);
if (reviewMatcher.find()) {
String countStr = reviewMatcher.group(1).replace(",", "");
int count = Integer.parseInt(countStr);
movie.setReviewCount(count);
movie.setBoxOffice(count * 0.5 + (Math.random() * 100));
}
}
movie.setQuote(item.select(".inq").text());
return movie;
}
}
```
#### 2.6.1 说明
- **封装**:`BASE_URL` 和 `USER_AGENT` 使用 `private static final`,外部无法访问和修改,是常量封装的标准做法。
- **封装**:`parseMovie()` 是 `private` 方法,把 HTML 解析细节隐藏起来。外部调用者只需知道 `crawl(limit)` 返回列表,不关心内部如何解析。
- 这是**方法级别的封装**——将复杂的实现细节隐藏在私有方法中,对外暴露简洁的公共接口。
---
### 2.7 `MovieService.java` / `DirectorController.java` - 组合与依赖注入
文件路径:`src/main/java/com/movieratings/service/MovieService.java`
```java
@Service
public class MovieService {
@Autowired
private MovieRepository movieRepository; // 组合:持有另一个对象的引用
@Cacheable(value = "directorRankings", key = "{#name, #type, #page, #size}")
public Page<DirectorStats> getDirectorRankings(String name, String type, int page, int size) {
Pageable pageable = PageRequest.of(page, size);
return movieRepository.findDirectorRankings(
(name == null || name.isEmpty()) ? null : name,
(type == null || type.isEmpty()) ? null : type,
pageable
);
}
@Transactional
@CacheEvict(value = {"directorRankings", "movieTypes"}, allEntries = true)
public void refreshData(List<Movie> movies) {
movieRepository.deleteAll();
movieRepository.saveAll(movies);
}
}
```
#### 2.7.1 说明
- **组合优于继承**:`MovieService` 没有继承 `MovieRepository`,而是通过 `@Autowired` **持有**它的引用。这是"组合"关系,是比继承更推荐的设计。
- 初学者应理解:**继承表示"是一个"(is-a)关系,组合表示"有一个"(has-a)关系。** 优先使用组合。
---
## 3. 概念对照表
| 概念 | 代码位置 | 说明 |
| --- | --- | --- |
| 封装 | `Movie.java`, `DirectorStats.java` | 字段 `private` + getter/setter |
| 封装 | `DataAnalyzer.java` 内部类 | 内部静态类的 `private` 字段 + getter |
| 封装 | `MovieCrawler.java` | `private static final` 常量 + `private` 方法 |
| 封装 | `MovieService.java` | 业务逻辑封装,隐藏 Repository 细节 |
| 继承 | `MovieRepository.java` | `extends JpaRepository<Movie, Long>` |
| 接口实现 | `DataInitializer.java` | `implements CommandLineRunner` |
| 组合 | `MovieService.java` | `@Autowired private MovieRepository` |
| 多态 | `Movie.toString()` / `DataInitializer` / `MovieRepository` | 运行时接口引用与子类方法重写 |
| 多态 | `DataAnalyzer.java` | Lambda 实现 `Comparator` 函数式接口 |
## 4. 项目代码中缺漏的 OOP 代码(重要)
当前项目代码能说明基本的封装、继承、多态概念,但作为**教学项目**,以下重要代码是缺失的。
建议初学者在理解本项目后,自行编写补充练习。
### 4.1 缺少 `equals()``hashCode()` 重写(已补充到源码)
`Movie.java``toString()` 已使用 `@Override`。本项目已在源码中补充 `equals()``hashCode()` 的配对重写示例。这是 Java OOP 的核心规则:
- 如果两个对象 `equals()` 为 true,它们的 `hashCode()` 必须相等。
- 重写 `equals()` 时必须同时重写 `hashCode()`,否则在 `HashMap`/`HashSet` 等集合中会出错。
项目中 `Movie.toString()` 已有 `@Override`,但以下为完整的方法重写对照示例:
```java
/**
* 完整的方法重写示例,注意每个方法都有 @Override 注解
*/
public class Movie {
// ... 字段省略 ...
/**
* 重写 Object 类的 toString() 方法
* @Override 告诉编译器:我在重写父类方法,如果拼写错误会报错
*/
@Override
public String toString() {
return title + " (" + releaseYear + ") - 评分: " + rating;
}
/**
* 重写 Object 类的 equals() 方法
* 两部电影 ID 相同就认为是同一部
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Movie other = (Movie) obj;
return id != null && id.equals(other.id);
}
/**
* 重写 hashCode() — equals 和 hashCode 必须配对重写
*/
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
}
```
> **初学者提示**:重写方法时始终加上 `@Override`,这样如果方法签名写错,编译器会报错。
---
### 4.2 缺少 `extends` 父类的显式类继承示例
项目中唯一的 `extends` 出现在 `MovieRepository extends JpaRepository`,这是一个**接口继承接口**。
项目中**没有** `class A extends class B` 的类继承示例。
**建议补充示例 — 类继承:**
```java
/**
* 父类:媒体作品(电影、电视剧、纪录片的共同父类)
*/
public class MediaWork {
private String title;
private double rating;
private int releaseYear;
private String director;
public MediaWork(String title, double rating, int releaseYear, String director) {
this.title = title;
this.rating = rating;
this.releaseYear = releaseYear;
this.director = director;
}
public String getTitle() { return title; }
public double getRating() { return rating; }
public int getReleaseYear() { return releaseYear; }
public String getDirector() { return director; }
@Override
public String toString() {
return title + " (" + releaseYear + ") 导演: " + director;
}
}
/**
* 子类:电影 — 继承 MediaWork,添加电影特有属性
*/
public class Movie extends MediaWork {
private String quote; // 电影特有:经典台词
private String country; // 电影特有:国家
private double boxOffice; // 电影特有:票房
public Movie(String title, double rating, int releaseYear, String director,
String quote, String country, double boxOffice) {
super(title, rating, releaseYear, director); // 调用父类构造方法
this.quote = quote;
this.country = country;
this.boxOffice = boxOffice;
}
public String getQuote() { return quote; }
public double getBoxOffice() { return boxOffice; }
@Override
public String toString() {
return super.toString() + " | 票房: " + boxOffice;
}
}
/**
* 子类:电视剧 — 继承 MediaWork,添加电视剧特有属性
*/
public class TVSeries extends MediaWork {
private int seasons; // 电视剧特有:季数
private int episodesPerSeason; // 电视剧特有:每季集数
public TVSeries(String title, double rating, int releaseYear, String director,
int seasons, int episodesPerSeason) {
super(title, rating, releaseYear, director);
this.seasons = seasons;
this.episodesPerSeason = episodesPerSeason;
}
public int getSeasons() { return seasons; }
@Override
public String toString() {
return super.toString() + " | " + seasons + "季";
}
}
```
**使用示例(体现多态):**
```java
// 多态:父类引用指向子类对象
MediaWork work1 = new Movie("肖申克的救赎", 9.7, 1994, "弗兰克·德拉邦特",
"希望让人自由", "美国", 5000);
MediaWork work2 = new TVSeries("绝命毒师", 9.5, 2008, "文斯·吉里根", 5, 13);
// 多态数组
MediaWork[] works = {work1, work2};
for (MediaWork work : works) {
// 运行时根据实际类型调用对应的 toString()
System.out.println(work.toString());
// 还可以访问父类定义的方法
System.out.println("评分: " + work.getRating());
}
```
> **初学者提示**
> - `super(...)` 调用父类构造方法,必须放在子类构造方法的**第一行**。
> - `@Override` 重写的方法,运行时会根据**实际对象类型**调用对应版本,这就是多态。
> - Java 中类只能**单继承**(一个类只能有一个直接父类),但可以实现多个接口。
---
### 4.3 缺少抽象类和抽象方法示例
项目中没有 `abstract` 关键字的使用。抽象类是理解继承和多态的重要桥梁。
**建议补充示例:**
```java
/**
* 抽象类:定义媒体作品的共同行为和强制子类实现的规则
* 抽象类不能被直接实例化,只能被继承
*/
public abstract class MediaWork {
private String title;
private double rating;
public MediaWork(String title, double rating) {
this.title = title;
this.rating = rating;
}
public String getTitle() { return title; }
public double getRating() { return rating; }
/**
* 抽象方法:没有方法体,强制子类提供自己的实现
* 不同媒体类型的展示格式不同,由各自子类决定
*/
public abstract String getDisplayFormat();
/**
* 普通方法:所有子类共享,不需要重写
*/
public String getRatingStars() {
int stars = (int) Math.round(rating / 2.0); // 5星制
return "★".repeat(stars) + "☆".repeat(5 - stars);
}
}
/**
* 具体子类:必须实现所有抽象方法
*/
public class Movie extends MediaWork {
private String country;
public Movie(String title, double rating, String country) {
super(title, rating);
this.country = country;
}
@Override
public String getDisplayFormat() {
return "电影: " + getTitle() + " [" + country + "] " + getRatingStars();
}
}
public class TVSeries extends MediaWork {
private int seasons;
public TVSeries(String title, double rating, int seasons) {
super(title, rating);
this.seasons = seasons;
}
@Override
public String getDisplayFormat() {
return "电视剧: " + getTitle() + " (" + seasons + "季) " + getRatingStars();
}
}
```
> **初学者提示**
> - 包含抽象方法的类**必须**声明为 `abstract`
> - 子类如果**不实现**所有抽象方法,也必须声明为 `abstract`
> - 抽象类和接口的区别:抽象类可以有构造方法和字段,接口(Java 8+)可以有默认方法但不能有实例字段。
---
### 4.4 缺少接口定义示例
项目中有 `implements`(实现接口)和 `extends`(接口继承接口),但**没有自己定义接口**。
**建议补充示例:**
```java
/**
* 接口:定义媒体作品应该具备的行为
* 接口中所有方法默认是 public abstract 的
*/
public interface MediaPlayable {
/** 播放 */
void play();
/** 暂停 */
void pause();
/** 停止 */
void stop();
/**
* 默认方法(Java 8+):提供默认实现,实现类可以选择性地覆盖
*/
default void showInfo() {
System.out.println("正在播放媒体内容...");
}
}
/**
* 实现接口
*/
public class MoviePlayer implements MediaPlayable {
private String title;
public MoviePlayer(String title) {
this.title = title;
}
@Override
public void play() {
System.out.println("开始播放电影: " + title);
}
@Override
public void pause() {
System.out.println("暂停播放: " + title);
}
@Override
public void stop() {
System.out.println("停止播放: " + title);
}
}
/**
* 多态示例:接口类型引用不同实现
*/
public class Demo {
public static void main(String[] args) {
MediaPlayable player = new MoviePlayer("肖申克的救赎");
player.play(); // 调用 MoviePlayer 的实现
player.pause();
player.showInfo(); // 调用接口的默认方法
}
}
```
> **初学者提示**
> - 接口定义"**能做什么**"(能力),类定义"**是什么**"(身份)。
> - 一个类可以实现**多个**接口(`implements A, B, C`),弥补了 Java 单继承的限制。
> - Java 8 起,接口可以有 `default` 方法和 `static` 方法。
---
### 4.5 缺漏总结
| 缺失的 OOP 代码 | 状态 | 说明 |
| --- | --- | --- |
| `@Override` 注解 | 已有 | `Movie.toString/equals/hashCode` 均使用 `@Override` |
| `equals()` / `hashCode()` 重写 | 已补充到源码 | `Movie.java` 已添加配对重写 |
| 类继承 (`class A extends class B`) | 缺失 | 只有接口继承,没有父类派生子类(见补充示例代码) |
| `super()` 调用父类构造 | 缺失 | 类继承中调用父类构造方法(见补充示例代码) |
| 抽象类 (`abstract class`) | 缺失 | 抽象方法和具体方法的混合(见补充示例代码) |
| 自定义接口 (`interface`) | 缺失 | 只有自己定义接口才能完整展示接口概念(见补充示例代码) |
| 多态数组 / 多态集合 | 缺失 | 父类引用数组指向不同子类对象(见补充示例代码) |
## 5. 初学者提示
- **封装**让你控制类的数据访问权限,不要直接访问对象的字段。
- **继承**让你复用已有行为,接口继承有助于解耦。优先使用接口继承而非类继承。
- **多态**让代码更灵活,写接口类型变量并在运行时指向不同实现。
- **组合优于继承**:能用 `has-a` 的地方就不要用 `is-a`
- **始终使用 `@Override`** 注解标注重写方法,避免拼写错误。
- 在这个项目中,`Movie` 和 `DirectorStats` 是数据模型类,`MovieRepository` 是持久化接口,`DataInitializer` 是程序启动时执行的初始化逻辑。
---
以上内容已经包含项目中所有与封装、继承、多态相关的主要代码段,并补充了项目中缺失的重要 OOP 代码示例。

20
project/src/main/java/com/movieratings/model/Movie.java

@ -91,4 +91,24 @@ public class Movie implements Serializable {
", type='" + type + '\'' + ", type='" + type + '\'' +
'}'; '}';
} }
/**
* 重写 equals() 两部电影 ID 相同则认为是同一部
* equals hashCode 必须配对重写
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Movie other = (Movie) obj;
return id != null && id.equals(other.id);
}
/**
* 重写 hashCode() equals() 保持一致基于 id 计算
*/
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0;
}
} }

BIN
project/target/classes/com/movieratings/model/Movie.class

Binary file not shown.

625
project/项目流程文档.md

@ -0,0 +1,625 @@
# 电影数据抓取与分析项目 - 流程文档
> 本文档面向 Java 初学者,详细说明项目的整体架构、数据流向和各模块职责。
---
## 1. 项目概述
### 1.1 项目目标
从豆瓣电影 Top 250 抓取影片数据,进行清洗、存储和多维度统计分析,最终以 Web 界面和图表形式展示结果。
### 1.2 技术栈
| 技术 | 版本 | 用途 |
| --- | --- | --- |
| Java | 11 | 主要开发语言 |
| Spring Boot | 2.7.12 | Web 框架、依赖注入 |
| Spring Data JPA | - | 数据持久化 |
| H2 Database | - | 内存数据库 |
| Jsoup | 1.15.3 | HTML 解析与网页爬取 |
| JFreeChart | 1.5.3 | 图表生成 |
| Apache Commons Math | 3.6.1 | 统计计算(相关性分析) |
| Jackson | - | JSON 序列化 |
| Thymeleaf | - | Web 模板引擎 |
| Caffeine | - | 缓存 |
### 1.3 两种运行模式
本项目支持两种独立的运行模式:
```
┌─────────────────────────────────────────────────────────────┐
│ 运行模式选择 │
├─────────────────────────────────────────────────────────────┤
│ 模式一:独立控制台模式 (Main.java) │
│ - 命令:mvn exec:java -Dexec.mainClass="com.movieratings.Main"│
│ - 输出:控制台表格 + JSON 文件 + PNG 图表 │
│ - 适用:数据分析演示、离线运行 │
├─────────────────────────────────────────────────────────────┤
│ 模式二:Spring Boot Web 模式 (MovieRatingsApplication.java) │
│ - 命令:mvn spring-boot:run │
│ - 输出:Web 界面 (http://localhost:8080) │
│ - 适用:交互式查询、在线展示 │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. 系统架构
### 2.1 整体架构图
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 项目整体架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 豆瓣电影 │────▶│ MovieCrawler │────▶│ Movie │ │
│ │ Top 250 │ │ (爬虫层) │ │ (模型层) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐│
│ ▼ ▼ ▼│
│ ┌──────────────────┐ ┌───────────────┐ ┌──────────┐│
│ │ DataAnalyzer │ │ MovieRepository│ │JSON/CSV ││
│ │ (分析层) │ │ (持久化层) │ │ (导出) ││
│ └──────────────────┘ └───────────────┘ └──────────┘│
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌───────────────┐ │
│ │ ResultDisplay │ │ MovieService │ │
│ │ (展示层) │ │ (业务层) │ │
│ └──────────────────┘ └───────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌───────────────┐ │
│ │ PNG 图表 + 控制台 │ │DirectorController│ │
│ │ (独立模式) │ │ (Web 控制器) │ │
│ └──────────────────┘ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Thymeleaf │ │
│ │ (Web 页面) │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
### 2.2 包结构
```
com.movieratings
├── Main.java # 独立模式入口
├── MovieRatingsApplication.java # Spring Boot 入口
├── DataInitializer.java # 启动时数据初始化
├── model/ # 数据模型层
│ ├── Movie.java # 电影实体类
│ └── DirectorStats.java # 导演统计 DTO
├── crawler/ # 爬虫层
│ └── MovieCrawler.java # 网页爬取与解析
├── analysis/ # 分析层
│ └── DataAnalyzer.java # 数据统计分析
├── display/ # 展示层
│ └── ResultDisplay.java # 控制台输出与图表生成
├── repository/ # 持久化层
│ └── MovieRepository.java # JPA 数据访问接口
├── service/ # 业务逻辑层
│ └── MovieService.java # 业务服务
└── controller/ # Web 控制层
└── DirectorController.java # HTTP 请求处理
```
---
## 3. 详细数据流程
### 3.1 模式一:独立控制台流程
```
┌─────────────────────────────────────────────────────────────────────┐
│ 独立模式执行流程 (Main.java) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 启动 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ MovieCrawler.crawl(50) │ │
│ │ ──────────────────────── │ │
│ │ • 连接豆瓣 Top 250 页面 │ │
│ │ • 解析 HTML (Jsoup) │ │
│ │ • 提取:排名、标题、评分、年份、导演等 │ │
│ │ • 返回 List<Movie> │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ DataAnalyzer (多维度分析) │ │
│ │ ──────────────────── │ │
│ │ • analyzeRatings() → 评分统计 │ │
│ │ • countMoviesByRatingRange() → 评分分布 │ │
│ │ • findMostReviewed() → 热门电影 │ │
│ │ • analyzeYearRatingCorrelation() → 相关性│ │
│ │ • getTopDirectors() → 导演排行 │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ ResultDisplay (结果展示) │ │
│ │ ────────────────── │ │
│ │ • printMoviesTable() → 控制台表格 │ │
│ │ • printDirectorRanking() → 导演排行榜 │ │
│ │ • generateRatingChart() → 评分分布图 │ │
│ │ • generateScatterPlot() → 年份评分散点图 │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ 数据导出 │ │
│ │ ──────── │ │
│ │ • saveAsJson() → movies_data.json │ │
│ │ • exportToCSV() → movies_analysis.csv │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 程序结束 │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### 3.2 模式二:Spring Boot Web 流程
```
┌─────────────────────────────────────────────────────────────────────┐
│ Spring Boot Web 模式执行流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 应用启动 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ MovieRatingsApplication.main() │ │
│ │ ──────────────────────────── │ │
│ │ • 启动 Spring 容器 │ │
│ │ • 初始化 H2 内存数据库 │ │
│ │ • 配置 Caffeine 缓存 │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ DataInitializer.run() (CommandLineRunner)│ │
│ │ ────────────────────────────── │ │
│ │ • 调用 MovieCrawler.crawl(100) │ │
│ │ • 设置部分作品类型(电影/电视剧/纪录片) │ │
│ │ • 调用 MovieService.refreshData() │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ MovieService.refreshData() │ │
│ │ ───────────────────── │ │
│ │ • movieRepository.deleteAll() │ │
│ │ • movieRepository.saveAll(movies) │ │
│ │ • 清除缓存 │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 应用就绪,等待 HTTP 请求 │
│ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 2. 用户请求处理 │
│ │
│ 浏览器访问 http://localhost:8080/directors │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ DirectorController.showDirectorRankings()│ │
│ │ ──────────────────────────────── │ │
│ │ • 接收参数:name, type, page, size │ │
│ │ • 调用 MovieService.getDirectorRankings()│ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ MovieService.getDirectorRankings() │ │
│ │ ──────────────────────────── │ │
│ │ • 检查缓存 (Caffeine) │ │
│ │ • 缓存未命中 → 调用 Repository │ │
│ │ • 返回 Page<DirectorStats> │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ MovieRepository.findDirectorRankings() │ │
│ │ ──────────────────────────────── │ │
│ │ • 执行 JPQL 聚合查询 │ │
│ │ • GROUP BY director │ │
│ │ • 返回导演统计数据 │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ Thymeleaf 模板渲染 │ │
│ │ ──────────────── │ │
│ │ • director_rankings.html │ │
│ │ • 返回 HTML 页面给浏览器 │ │
│ └──────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 4. 核心模块详解
### 4.1 爬虫模块 (MovieCrawler)
**职责**:从豆瓣电影 Top 250 抓取数据
**流程**:
```
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 构造请求URL │───▶│ 发送HTTP请求│───▶│ 解析HTML │───▶│ 封装Movie │
│ (分页参数) │ │ (Jsoup) │ │ (CSS选择器)│ │ 对象列表 │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
```
**关键代码**:
```java
// 发送请求
Document doc = Jsoup.connect(url).userAgent(USER_AGENT).get();
// 解析数据
movie.setRank(Integer.parseInt(item.select(".pic em").text()));
movie.setTitle(item.select(".title").first().text());
movie.setRating(Double.parseDouble(item.select(".rating_num").text()));
```
**数据提取字段**:
| 字段 | 提取方式 |
| --- | --- |
| 排名 | `.pic em` |
| 标题 | `.title` |
| 评分 | `.rating_num` |
| 海报 | `.pic img` 的 src 属性 |
| 年份 | 正则匹配 `\d{4}` |
| 导演 | 文本中提取 `导演: xxx` |
| 国家 | 按 `/` 分割取倒数第二项 |
| 评价人数 | 正则匹配 `([\d,]+)人评价` |
---
### 4.2 数据模型 (Movie)
**职责**:定义电影数据的结构,使用 JPA 注解映射到数据库
**类图**:
```
┌─────────────────────────────────────────┐
│ Movie │
├─────────────────────────────────────────┤
│ - id: Long (主键, 自增) │
│ - title: String (标题) │
│ - rating: double (评分) │
│ - releaseYear: int (年份) │
│ - rank: int (排名) │
│ - quote: String (简评) │
│ - director: String (导演) │
│ - reviewCount: int (评价人数) │
│ - country: String (国家) │
│ - boxOffice: double (票房) │
│ - type: String (类型) │
│ - posterUrl: String (海报链接) │
├─────────────────────────────────────────┤
│ + getter/setter 方法 │
│ + toString(): String │
│ + equals(Object): boolean │
│ + hashCode(): int │
└─────────────────────────────────────────┘
```
**JPA 注解说明**:
- `@Entity` — 标记为数据库实体
- `@Table(name = "movies")` — 指定表名
- `@Id` + `@GeneratedValue` — 主键自增策略
---
### 4.3 数据分析模块 (DataAnalyzer)
**职责**:对电影数据进行多维度统计分析
**分析方法**:
```
┌──────────────────────────────────────────────────────────────────┐
│ DataAnalyzer 方法 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ analyzeRatings(List<Movie>) │
│ └──▶ 返回 DoubleSummaryStatistics (平均值、最大值、最小值、计数) │
│ │
│ countMoviesByRatingRange(List<Movie>) │
│ └──▶ 返回 Map<String, Long> (评分段 → 电影数量) │
│ 评分段: 9.5-10.0, 9.0-9.4, 8.5-8.9, 8.5以下 │
│ │
│ findMostReviewed(List<Movie>, int n) │
│ └──▶ 返回评价人数最多的前 N 部电影 │
│ │
│ analyzeYearRatingCorrelation(List<Movie>) │
│ └──▶ 返回 CorrelationResult (Pearson 相关系数 + 显著性检验) │
│ │
│ getTopDirectors(List<Movie>, int topN) │
│ └──▶ 返回 List<DirectorStats> (按作品数排序的导演排行) │
│ │
└──────────────────────────────────────────────────────────────────┘
```
**内部类**:
- `DirectorStats` — 导演统计结果(姓名、作品数、平均分、总票房)
- `CorrelationResult` — 相关性分析结果(系数、p 值、显著性描述)
---
### 4.4 数据持久化层
**MovieRepository 接口**:
```
┌─────────────────────────────────────────────────────────────────┐
│ MovieRepository │
│ extends JpaRepository<Movie, Long>
├─────────────────────────────────────────────────────────────────┤
│ │
│ 继承自 JpaRepository 的方法: │
│ • save(Movie), saveAll(List<Movie>) │
│ • deleteAll(), deleteById(Long) │
│ • findById(Long), findAll() │
│ │
│ 自定义方法: │
│ • findDirectorRankings(name, type, pageable) │
│ └── JPQL 聚合查询,返回导演排行榜 │
│ • findByDirector(String director) │
│ └── 按导演名查询作品列表 │
│ • findAllTypes() │
│ └── 查询所有不同的作品类型 │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**JPQL 聚合查询示例**:
```java
@Query("SELECT new com.movieratings.model.DirectorStats(" +
"m.director, COUNT(m), MAX(m.posterUrl), AVG(m.rating), SUM(m.boxOffice)) " +
"FROM Movie m " +
"WHERE (:name IS NULL OR m.director LIKE %:name%) " +
"GROUP BY m.director " +
"ORDER BY COUNT(m) DESC")
Page<DirectorStats> findDirectorRankings(...);
```
---
### 4.5 业务服务层 (MovieService)
**职责**:封装业务逻辑,管理缓存和事务
**方法说明**:
| 方法 | 功能 | 缓存策略 |
| --- | --- | --- |
| `getDirectorRankings()` | 获取导演排行榜 | `@Cacheable` 缓存 10 分钟 |
| `getAllTypes()` | 获取所有作品类型 | `@Cacheable` 缓存 |
| `saveAll()` | 批量保存电影 | `@CacheEvict` 清除缓存 |
| `refreshData()` | 清空并重新加载数据 | `@CacheEvict` 清除缓存 |
---
### 4.6 Web 控制层 (DirectorController)
**职责**:处理 HTTP 请求,返回 Web 页面
**路由映射**:
| URL | 方法 | 功能 |
| --- | --- | --- |
| `GET /` | `index()` | 重定向到导演排行 |
| `GET /directors` | `showDirectorRankings()` | 导演排行榜页面 |
| `GET /director/{name}` | `showDirectorMovies()` | 指定导演的作品列表 |
**请求参数**:
```
GET /directors?name=张艺谋&type=电影&page=0&size=20
```
---
## 5. 数据流图
### 5.1 数据抓取流程
```
豆瓣服务器 本地应用
│ │
│ 1. HTTP GET 请求 │
│◀──────────────────────────│
│ │
│ 2. HTML 响应 │
│──────────────────────────▶│
│ │
│ ┌────┴────┐
│ │ Jsoup │
│ │ 解析 │
│ └────┬────┘
│ │
│ ┌────┴────┐
│ │ Movie │
│ │ 对象 │
│ └────┬────┘
│ │
│ 存入 List<Movie>
```
### 5.2 Web 请求处理流程
```
浏览器 服务器
│ │
│ GET /directors │
│──────────────────────────▶│
│ ┌────┴────┐
│ │Controller│
│ └────┬────┘
│ │
│ ┌────┴────┐
│ │ Service │
│ │ (检查缓存)│
│ └────┬────┘
│ │
│ ┌────┴────┐
│ │Repository│
│ │ (查询DB) │
│ └────┬────┘
│ │
│ ┌────┴────┐
│ │Thymeleaf│
│ │ 渲染 │
│ └────┬────┘
│ │
│ HTML 响应 │
│◀──────────────────────────│
```
---
## 6. 配置说明
### 6.1 数据库配置 (application.properties)
```properties
# H2 内存数据库
spring.datasource.url=jdbc:h2:mem:moviedb;DB_CLOSE_DELAY=-1
spring.h2.console.enabled=true # 启用 H2 控制台
# JPA 配置
spring.jpa.hibernate.ddl-auto=update # 自动创建表结构
```
### 6.2 缓存配置
```properties
spring.cache.type=caffeine
spring.cache.cache-names=directorRankings,movieTypes
spring.cache.caffeine.spec=expireAfterWrite=10m,maximumSize=100
```
- 缓存 10 分钟后过期
- 最大缓存 100 条记录
---
## 7. 运行指南
### 7.1 环境要求
- JDK 11+
- Maven 3.6+
### 7.2 运行命令
**独立控制台模式**:
```bash
mvn clean compile exec:java -Dexec.mainClass="com.movieratings.Main"
```
**Spring Boot Web 模式**:
```bash
mvn spring-boot:run
```
### 7.3 访问地址
| 功能 | 地址 |
| --- | --- |
| 导演排行榜 | http://localhost:8080/directors |
| H2 数据库控制台 | http://localhost:8080/h2-console |
| JDBC URL | `jdbc:h2:mem:moviedb` |
---
## 8. 输出文件说明
### 8.1 独立模式输出
| 文件 | 说明 |
| --- | --- |
| `movies_data.json` | 完整的电影数据 JSON |
| `movies_analysis.csv` | 电影数据 CSV 表格 |
| `rating_distribution.png` | 评分分布柱状图 |
| `year_rating_scatter.png` | 年份-评分散点图 |
### 8.2 控制台输出示例
```
=== 电影数据抓取与分析项目开始 ===
正在抓取: https://movie.douban.com/top250?start=0&filter=
...
--- 电影抓取结果展示 (前 10 条展示) ---
--------------------------------------------------------------------------------------------------
| 排名 | 标题 | 年份 | 评分 | 导演 | 评价人数 |
--------------------------------------------------------------------------------------------------
| 1 | 肖申克的救赎 | 1994 | 9.7 | 弗兰克·德拉邦特 | 2900000 |
| 2 | 霸王别姬 | 1993 | 9.6 | 陈凯歌 | 1900000 |
...
--- 基础统计分析报告 ---
总计分析电影数量: 50
平均评分: 8.92
最高评分: 9.70
最低评分: 8.50
--- 导演作品排行榜 (前 20) ---
------------------------------------------------------------------
| 导演 | 作品数 | 平均分 | 总模拟票房 |
------------------------------------------------------------------
| 克里斯托弗·诺兰 | 7 | 8.9 | 1250000.00 |
| 斯蒂芬·斯皮尔伯格 | 6 | 8.7 | 980000.00 |
...
=== 项目执行完毕 ===
```
---
## 9. 扩展阅读
### 9.1 相关文档
- `OOP_封装_继承_多态.md` — 项目中的面向对象概念详解
- `README.md` — 项目基本信息
- `DEVELOPMENT.md` — 开发日志
### 9.2 学习要点
1. **爬虫技术**:Jsoup 的使用、HTTP 请求、HTML 解析
2. **数据持久化**:JPA 实体映射、Repository 模式
3. **业务分层**:Controller → Service → Repository 架构
4. **缓存机制**:Caffeine 缓存的使用场景
5. **数据分析**:Stream API、统计计算、相关性分析
6. **可视化**:JFreeChart 图表生成
---
*文档版本:1.0*
*更新日期:2026-04-09*

72
w5/README.md

@ -0,0 +1,72 @@
# Java程序设计 第5周作业
## 一、作业内容
本周作业主要练习Java中的**抽象类**和**继承**,通过两个实例理解面向对象程序设计中多态的应用。
## 二、项目结构
```
w5/
├── shape/ # 图形绘制示例
│ ├── Shape.java # 抽象父类
│ ├── Circle.java # 圆形子类
│ ├── Rectangle.java # 矩形子类
│ └── Main.java # 测试类
├── vehicle/ # 交通工具示例
│ ├── Vehicle.java # 抽象父类
│ ├── Car.java # 汽车子类
│ ├── Bike.java # 自行车子类
│ ├── Truck.java # 卡车子类
│ └── Main.java # 测试类
└── README.md # 说明文档
```
## 三、知识点总结
### 1. 抽象类
- 使用`abstract`关键字修饰的类称为抽象类
- 抽象类不能被实例化
- 抽象类可以包含抽象方法和具体方法
- 抽象方法只有声明,没有实现
### 2. 继承
- 使用`extends`关键字实现继承
- 子类继承父类的属性和方法
- 子类必须实现父类的所有抽象方法(除非子类也是抽象类)
### 3. 多态
- 父类引用可以指向子类对象(向上转型)
- 运行时根据实际对象类型调用相应的方法
## 四、运行方法
### shape包:
```bash
cd w5
javac shape/*.java
java shape.Main
```
### vehicle包:
```bash
cd w5
javac vehicle/*.java
java vehicle.Main
```
## 五、运行结果
详见`输出结果.txt`文件。
## 六、心得体会
1. 抽象类为子类提供了一个公共的模板,定义了子类必须实现的方法。
2. 通过继承可以复用代码,减少重复。
3. 多态使得程序更加灵活,可以用统一的接口操作不同的对象。
---
作者:[学生姓名]
日期:2026年4月

BIN
w5/shape.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
w5/shape/Circle.class

Binary file not shown.

6
w5/shape/Circle.java

@ -0,0 +1,6 @@
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}

BIN
w5/shape/Main.class

Binary file not shown.

8
w5/shape/Main.java

@ -0,0 +1,8 @@
public class Main {
public static void main(String[] args) {
Shape circle = new Circle();
Shape rectangle = new Rectangle();
circle.draw();
rectangle.draw();
}
}

BIN
w5/shape/Rectangle.class

Binary file not shown.

6
w5/shape/Rectangle.java

@ -0,0 +1,6 @@
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}

BIN
w5/shape/Shape.class

Binary file not shown.

3
w5/shape/Shape.java

@ -0,0 +1,3 @@
public abstract class Shape {
public abstract void draw();
}

BIN
w5/vehicle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
w5/vehicle/Bike.class

Binary file not shown.

6
w5/vehicle/Bike.java

@ -0,0 +1,6 @@
public class Bike extends Vehicle {
@Override
public void run() {
System.out.println("Running a bike");
}
}

BIN
w5/vehicle/Car.class

Binary file not shown.

6
w5/vehicle/Car.java

@ -0,0 +1,6 @@
public class Car extends Vehicle {
@Override
public void run() {
System.out.println("Running a car");
}
}

BIN
w5/vehicle/Main.class

Binary file not shown.

10
w5/vehicle/Main.java

@ -0,0 +1,10 @@
public class Main {
public static void main(String[] args) {
Vehicle car = new Car();
car.run();
Vehicle bike = new Bike();
bike.run();
Vehicle truck = new Truck();
truck.run();
}
}

BIN
w5/vehicle/Truck.class

Binary file not shown.

6
w5/vehicle/Truck.java

@ -0,0 +1,6 @@
public class Truck extends Vehicle {
@Override
public void run() {
System.out.println("Running a truck");
}
}

BIN
w5/vehicle/Vehicle.class

Binary file not shown.

3
w5/vehicle/Vehicle.java

@ -0,0 +1,3 @@
public abstract class Vehicle {
public abstract void run();
}

BIN
w6/Animal.class

Binary file not shown.

3
w6/Animal.java

@ -0,0 +1,3 @@
public abstract class Animal {
public abstract void makeSound();
}

BIN
w6/Animal.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
w6/Cat.class

Binary file not shown.

6
w6/Cat.java

@ -0,0 +1,6 @@
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}

BIN
w6/Dog.class

Binary file not shown.

11
w6/Dog.java

@ -0,0 +1,11 @@
public class Dog extends Animal implements Swimmable {
@Override
public void makeSound() {
System.out.println("Woof!");
}
@Override
public void swim() {
System.out.println("The dog is swimming!");
}
}

BIN
w6/Main.class

Binary file not shown.

11
w6/Main.java

@ -0,0 +1,11 @@
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
dog.makeSound(); // Output: Woof!
cat.makeSound(); // Output: Meow!
dog.swim(); // Output: The dog is swimming!
}
}

75
w6/README.md

@ -0,0 +1,75 @@
# Java程序设计 第6周作业
## 一、作业内容
本周作业主要练习Java中的**接口(Interface)**,理解接口与抽象类的区别,以及类同时继承抽象类和实现接口的用法。
## 二、项目结构
```
w6/
├── Animal.java # 抽象父类 - 动物
├── Swimmable.java # 接口 - 可游泳
├── Dog.java # 狗类(继承Animal,实现Swimmable)
├── Cat.java # 猫类(继承Animal)
├── Main.java # 测试类
└── README.md # 说明文档
```
## 三、知识点总结
### 1. 抽象类
- 使用`abstract`关键字修饰
- 可以包含抽象方法和具体方法
- 不能被实例化
- 子类使用`extends`继承
### 2. 接口
- 使用`interface`关键字定义
- 接口中的方法默认为`public abstract`
- 类使用`implements`实现接口
- 一个类可以实现多个接口
### 3. 接口与抽象类的区别
| 特性 | 抽象类 | 接口 |
|------|--------|------|
| 关键字 | abstract | interface |
| 继承/实现 | extends | implements |
| 多继承 | 单继承 | 可实现多个 |
| 方法 | 可有具体方法 | 默认抽象方法 |
| 变量 | 可有成员变量 | 只能是常量 |
### 4. 本项目设计思路
- `Animal`抽象类:定义所有动物的共性(发出声音)
- `Swimmable`接口:定义会游泳的能力
- `Dog`类:继承Animal,同时实现Swimmable接口(狗会游泳)
- `Cat`类:只继承Animal(猫不会游泳)
## 四、运行方法
```bash
cd w6
javac *.java
java Main
```
## 五、运行结果
```
Woof!
Meow!
The dog is swimming!
```
## 六、心得体会
1. 接口定义了一组行为规范,实现接口的类必须提供具体实现。
2. Java不支持多继承,但一个类可以实现多个接口,弥补了单继承的局限性。
3. 通过接口可以实现"行为"的抽象,如Swimmable表示"会游泳"这个能力,任何类都可以实现这个接口。
---
作者:[学生姓名]
日期:2026年4月

BIN
w6/Swimmable.class

Binary file not shown.

3
w6/Swimmable.java

@ -0,0 +1,3 @@
public interface Swimmable {
void swim();
}
Loading…
Cancel
Save