# 封装、继承、多态(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 代码示例。