38 changed files with 1800 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"java.compile.nullAnalysis.mode": "automatic" |
||||
|
} |
||||
@ -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 代码示例。 |
||||
Binary file not shown.
@ -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* |
||||
@ -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月 |
||||
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
public class Circle extends Shape { |
||||
|
@Override |
||||
|
public void draw() { |
||||
|
System.out.println("Drawing a circle"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -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(); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
public class Rectangle extends Shape { |
||||
|
@Override |
||||
|
public void draw() { |
||||
|
System.out.println("Drawing a rectangle"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,3 @@ |
|||||
|
public abstract class Shape { |
||||
|
public abstract void draw(); |
||||
|
} |
||||
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
public class Bike extends Vehicle { |
||||
|
@Override |
||||
|
public void run() { |
||||
|
System.out.println("Running a bike"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
public class Car extends Vehicle { |
||||
|
@Override |
||||
|
public void run() { |
||||
|
System.out.println("Running a car"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -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(); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
public class Truck extends Vehicle { |
||||
|
@Override |
||||
|
public void run() { |
||||
|
System.out.println("Running a truck"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,3 @@ |
|||||
|
public abstract class Vehicle { |
||||
|
public abstract void run(); |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,3 @@ |
|||||
|
public abstract class Animal { |
||||
|
public abstract void makeSound(); |
||||
|
} |
||||
|
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
public class Cat extends Animal { |
||||
|
@Override |
||||
|
public void makeSound() { |
||||
|
System.out.println("Meow!"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -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!"); |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -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!
|
||||
|
} |
||||
|
} |
||||
@ -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月 |
||||
Binary file not shown.
@ -0,0 +1,3 @@ |
|||||
|
public interface Swimmable { |
||||
|
void swim(); |
||||
|
} |
||||
Loading…
Reference in new issue