You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

32 KiB

封装、继承、多态(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

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(),否则在 HashMapHashSet 等集合中会出错。
    • getClass() != obj.getClass() 确保只比较同类型的对象。

2.2 DirectorStats.java - 封装示例

文件路径:src/main/java/com/movieratings/model/DirectorStats.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

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

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 说明

  • 封装DirectorStatsCorrelationResultDataAnalyzer内部静态类,字段为 private,通过 public getter 暴露数据,是封装的典型应用。
  • 封装getSignificance() 方法把 p 值的判断逻辑封装在类内部,外部只需调用方法,不需要知道判断规则。
  • 封装:各个分析方法(analyzeRatingscountMoviesByRatingRange 等)将复杂的数据处理逻辑封装为单一方法调用。
  • 多态sorted((m1, m2) -> ...) 使用了函数式接口 Comparator,这是多态在 Java 8+ 中的体现——接口引用指向 Lambda 表达式实现。

2.6 MovieCrawler.java - 常量封装与私有方法封装

文件路径:src/main/java/com/movieratings/crawler/MovieCrawler.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_URLUSER_AGENT 使用 private static final,外部无法访问和修改,是常量封装的标准做法。
  • 封装parseMovie()private 方法,把 HTML 解析细节隐藏起来。外部调用者只需知道 crawl(limit) 返回列表,不关心内部如何解析。
  • 这是方法级别的封装——将复杂的实现细节隐藏在私有方法中,对外暴露简洁的公共接口。

2.7 MovieService.java / DirectorController.java - 组合与依赖注入

文件路径:src/main/java/com/movieratings/service/MovieService.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.javatoString() 已使用 @Override。本项目已在源码中补充 equals()hashCode() 的配对重写示例。这是 Java OOP 的核心规则:

  • 如果两个对象 equals() 为 true,它们的 hashCode() 必须相等。
  • 重写 equals() 时必须同时重写 hashCode(),否则在 HashMap/HashSet 等集合中会出错。

项目中 Movie.toString() 已有 @Override,但以下为完整的方法重写对照示例:

/**
 * 完整的方法重写示例,注意每个方法都有 @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 的类继承示例。

建议补充示例 — 类继承:

/**
 * 父类:媒体作品(电影、电视剧、纪录片的共同父类)
 */
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 + "季";
    }
}

使用示例(体现多态):

// 多态:父类引用指向子类对象
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 关键字的使用。抽象类是理解继承和多态的重要桥梁。

建议补充示例:

/**
 * 抽象类:定义媒体作品的共同行为和强制子类实现的规则
 * 抽象类不能被直接实例化,只能被继承
 */
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(接口继承接口),但没有自己定义接口

建议补充示例:

/**
 * 接口:定义媒体作品应该具备的行为
 * 接口中所有方法默认是 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 注解标注重写方法,避免拼写错误。
  • 在这个项目中,MovieDirectorStats 是数据模型类,MovieRepository 是持久化接口,DataInitializer 是程序启动时执行的初始化逻辑。

以上内容已经包含项目中所有与封装、继承、多态相关的主要代码段,并补充了项目中缺失的重要 OOP 代码示例。