diff --git a/project/.vscode/settings.json b/project/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/project/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/project/OOP_封装_继承_多态.md b/project/OOP_封装_继承_多态.md new file mode 100644 index 0000000..6f85684 --- /dev/null +++ b/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 { + + /** + * 按导演统计作品数量排行榜,支持搜索、作品类型过滤和分页 + */ + @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 findDirectorRankings(@Param("name") String name, @Param("type") String type, Pageable pageable); + + /** + * 获取指定导演的作品列表 + */ + List findByDirector(String director); + + /** + * 获取所有不同的作品类型 + */ + @Query("SELECT DISTINCT m.type FROM Movie m WHERE m.type IS NOT NULL") + List findAllTypes(); +} +``` + +#### 2.3.1 说明 +- `MovieRepository` 通过 `extends JpaRepository` 继承了 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 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 getTopDirectors(List movies, int topN) { + Map> directorMap = movies.stream() + .filter(m -> m.getDirector() != null) + .collect(Collectors.groupingBy(Movie::getDirector)); + + return directorMap.entrySet().stream() + .map(entry -> { + String name = entry.getKey(); + List 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 movies) { + return movies.stream() + .mapToDouble(Movie::getRating) + .summaryStatistics(); + } + + /** + * 按评分段统计 + */ + public Map countMoviesByRatingRange(List 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 findMostReviewed(List 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 crawl(int limit) { + List 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 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 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` | +| 接口实现 | `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 代码示例。 \ No newline at end of file diff --git a/project/src/main/java/com/movieratings/model/Movie.java b/project/src/main/java/com/movieratings/model/Movie.java index 74ae307..1f965d6 100644 --- a/project/src/main/java/com/movieratings/model/Movie.java +++ b/project/src/main/java/com/movieratings/model/Movie.java @@ -91,4 +91,24 @@ public class Movie implements Serializable { ", 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; + } } diff --git a/project/target/classes/com/movieratings/model/Movie.class b/project/target/classes/com/movieratings/model/Movie.class index b0953ae..80fcc64 100644 Binary files a/project/target/classes/com/movieratings/model/Movie.class and b/project/target/classes/com/movieratings/model/Movie.class differ diff --git a/project/项目流程文档.md b/project/项目流程文档.md new file mode 100644 index 0000000..2417805 --- /dev/null +++ b/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 │ │ +│ └──────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ 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 │ │ +│ └──────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ 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) │ +│ └──▶ 返回 DoubleSummaryStatistics (平均值、最大值、最小值、计数) │ +│ │ +│ countMoviesByRatingRange(List) │ +│ └──▶ 返回 Map (评分段 → 电影数量) │ +│ 评分段: 9.5-10.0, 9.0-9.4, 8.5-8.9, 8.5以下 │ +│ │ +│ findMostReviewed(List, int n) │ +│ └──▶ 返回评价人数最多的前 N 部电影 │ +│ │ +│ analyzeYearRatingCorrelation(List) │ +│ └──▶ 返回 CorrelationResult (Pearson 相关系数 + 显著性检验) │ +│ │ +│ getTopDirectors(List, int topN) │ +│ └──▶ 返回 List (按作品数排序的导演排行) │ +│ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**内部类**: +- `DirectorStats` — 导演统计结果(姓名、作品数、平均分、总票房) +- `CorrelationResult` — 相关性分析结果(系数、p 值、显著性描述) + +--- + +### 4.4 数据持久化层 + +**MovieRepository 接口**: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ MovieRepository │ +│ extends JpaRepository │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ 继承自 JpaRepository 的方法: │ +│ • save(Movie), saveAll(List) │ +│ • 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 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 +``` + +### 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* diff --git a/w5/README.md b/w5/README.md new file mode 100644 index 0000000..1635a07 --- /dev/null +++ b/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月 diff --git a/w5/shape.png b/w5/shape.png new file mode 100644 index 0000000..c89cb4b Binary files /dev/null and b/w5/shape.png differ diff --git a/w5/shape/Circle.class b/w5/shape/Circle.class new file mode 100644 index 0000000..b47c995 Binary files /dev/null and b/w5/shape/Circle.class differ diff --git a/w5/shape/Circle.java b/w5/shape/Circle.java new file mode 100644 index 0000000..ac055d8 --- /dev/null +++ b/w5/shape/Circle.java @@ -0,0 +1,6 @@ +public class Circle extends Shape { + @Override + public void draw() { + System.out.println("Drawing a circle"); + } +} diff --git a/w5/shape/Main.class b/w5/shape/Main.class new file mode 100644 index 0000000..11ff8f7 Binary files /dev/null and b/w5/shape/Main.class differ diff --git a/w5/shape/Main.java b/w5/shape/Main.java new file mode 100644 index 0000000..4e29359 --- /dev/null +++ b/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(); + } +} diff --git a/w5/shape/Rectangle.class b/w5/shape/Rectangle.class new file mode 100644 index 0000000..63a2d6e Binary files /dev/null and b/w5/shape/Rectangle.class differ diff --git a/w5/shape/Rectangle.java b/w5/shape/Rectangle.java new file mode 100644 index 0000000..c5f6f06 --- /dev/null +++ b/w5/shape/Rectangle.java @@ -0,0 +1,6 @@ +public class Rectangle extends Shape { + @Override + public void draw() { + System.out.println("Drawing a rectangle"); + } +} diff --git a/w5/shape/Shape.class b/w5/shape/Shape.class new file mode 100644 index 0000000..43358a0 Binary files /dev/null and b/w5/shape/Shape.class differ diff --git a/w5/shape/Shape.java b/w5/shape/Shape.java new file mode 100644 index 0000000..18bcb4e --- /dev/null +++ b/w5/shape/Shape.java @@ -0,0 +1,3 @@ +public abstract class Shape { + public abstract void draw(); +} diff --git a/w5/vehicle.png b/w5/vehicle.png new file mode 100644 index 0000000..0d7d2a1 Binary files /dev/null and b/w5/vehicle.png differ diff --git a/w5/vehicle/Bike.class b/w5/vehicle/Bike.class new file mode 100644 index 0000000..194221c Binary files /dev/null and b/w5/vehicle/Bike.class differ diff --git a/w5/vehicle/Bike.java b/w5/vehicle/Bike.java new file mode 100644 index 0000000..5bb1c5f --- /dev/null +++ b/w5/vehicle/Bike.java @@ -0,0 +1,6 @@ +public class Bike extends Vehicle { + @Override + public void run() { + System.out.println("Running a bike"); + } +} diff --git a/w5/vehicle/Car.class b/w5/vehicle/Car.class new file mode 100644 index 0000000..6a31bb3 Binary files /dev/null and b/w5/vehicle/Car.class differ diff --git a/w5/vehicle/Car.java b/w5/vehicle/Car.java new file mode 100644 index 0000000..b49131c --- /dev/null +++ b/w5/vehicle/Car.java @@ -0,0 +1,6 @@ +public class Car extends Vehicle { + @Override + public void run() { + System.out.println("Running a car"); + } +} diff --git a/w5/vehicle/Main.class b/w5/vehicle/Main.class new file mode 100644 index 0000000..5a4cae2 Binary files /dev/null and b/w5/vehicle/Main.class differ diff --git a/w5/vehicle/Main.java b/w5/vehicle/Main.java new file mode 100644 index 0000000..4deff8d --- /dev/null +++ b/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(); + } +} diff --git a/w5/vehicle/Truck.class b/w5/vehicle/Truck.class new file mode 100644 index 0000000..3896f99 Binary files /dev/null and b/w5/vehicle/Truck.class differ diff --git a/w5/vehicle/Truck.java b/w5/vehicle/Truck.java new file mode 100644 index 0000000..8ab43f1 --- /dev/null +++ b/w5/vehicle/Truck.java @@ -0,0 +1,6 @@ +public class Truck extends Vehicle { + @Override + public void run() { + System.out.println("Running a truck"); + } +} diff --git a/w5/vehicle/Vehicle.class b/w5/vehicle/Vehicle.class new file mode 100644 index 0000000..37d3e50 Binary files /dev/null and b/w5/vehicle/Vehicle.class differ diff --git a/w5/vehicle/Vehicle.java b/w5/vehicle/Vehicle.java new file mode 100644 index 0000000..ecef84f --- /dev/null +++ b/w5/vehicle/Vehicle.java @@ -0,0 +1,3 @@ +public abstract class Vehicle { + public abstract void run(); +} diff --git a/w6/Animal.class b/w6/Animal.class new file mode 100644 index 0000000..b05489d Binary files /dev/null and b/w6/Animal.class differ diff --git a/w6/Animal.java b/w6/Animal.java new file mode 100644 index 0000000..186a13f --- /dev/null +++ b/w6/Animal.java @@ -0,0 +1,3 @@ +public abstract class Animal { + public abstract void makeSound(); +} diff --git a/w6/Animal.png b/w6/Animal.png new file mode 100644 index 0000000..a938783 Binary files /dev/null and b/w6/Animal.png differ diff --git a/w6/Cat.class b/w6/Cat.class new file mode 100644 index 0000000..6035d14 Binary files /dev/null and b/w6/Cat.class differ diff --git a/w6/Cat.java b/w6/Cat.java new file mode 100644 index 0000000..ab22387 --- /dev/null +++ b/w6/Cat.java @@ -0,0 +1,6 @@ +public class Cat extends Animal { + @Override + public void makeSound() { + System.out.println("Meow!"); + } +} diff --git a/w6/Dog.class b/w6/Dog.class new file mode 100644 index 0000000..867983a Binary files /dev/null and b/w6/Dog.class differ diff --git a/w6/Dog.java b/w6/Dog.java new file mode 100644 index 0000000..b97a5fc --- /dev/null +++ b/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!"); + } +} diff --git a/w6/Main.class b/w6/Main.class new file mode 100644 index 0000000..60c7df5 Binary files /dev/null and b/w6/Main.class differ diff --git a/w6/Main.java b/w6/Main.java new file mode 100644 index 0000000..593fe29 --- /dev/null +++ b/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! + } +} diff --git a/w6/README.md b/w6/README.md new file mode 100644 index 0000000..a832a64 --- /dev/null +++ b/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月 diff --git a/w6/Swimmable.class b/w6/Swimmable.class new file mode 100644 index 0000000..5650772 Binary files /dev/null and b/w6/Swimmable.class differ diff --git a/w6/Swimmable.java b/w6/Swimmable.java new file mode 100644 index 0000000..78dc9de --- /dev/null +++ b/w6/Swimmable.java @@ -0,0 +1,3 @@ +public interface Swimmable { + void swim(); +}