@ -1,4 +0,0 @@ |
|||
*.jar |
|||
*.jar |
|||
*.class |
|||
*.log |
|||
@ -0,0 +1,5 @@ |
|||
public class HelloWorld { |
|||
public static void main(String[] args) { |
|||
System.out.println("Hello World!"); |
|||
} |
|||
} |
|||
@ -1,17 +1,10 @@ |
|||
# DataCollect 教学项目 — 最小可运行版本 |
|||
|
|||
这是一个最小可用的 Java CLI 演示工程,目标:打印帮助信息以验证运行环境。 |
|||
|
|||
构建: |
|||
```bash |
|||
mvn -q package |
|||
``` |
|||
|
|||
运行(示例): |
|||
```bash |
|||
java -jar target/datacollect-cli-0.1.0-jar-with-dependencies.jar --help |
|||
``` |
|||
|
|||
项目结构(最小): |
|||
- `src/main/java/com/example/datacollect/Main.java` — CLI 入口,打印帮助 |
|||
- `pom.xml` — Maven 构建配置,生成可执行 jar |
|||
# java |
|||
温度转换程序: |
|||
额外支持:命令参数模式,文件参量转换模式 |
|||
编译命令: |
|||
javac TemperatureConverter.java |
|||
运行命令: |
|||
Java TemperatureConverter |
|||
示例: |
|||
请输入要转换的温度与单位:36.6C |
|||
36.6C=97.88F |
|||
@ -0,0 +1,67 @@ |
|||
import java.util.Scanner; |
|||
|
|||
/** |
|||
* TemperatureConverter |
|||
* 支持摄氏度(C)与华氏度(F)之间互转 |
|||
*/ |
|||
public class TemperatureConverter { |
|||
|
|||
/** |
|||
* 将摄氏度转换为华氏度 |
|||
* @param c 摄氏温度 |
|||
* @return 对应的华氏温度 |
|||
*/ |
|||
public static double celsiusToFahrenheit(double c) { |
|||
return c * 9.0 / 5.0 + 32.0; |
|||
} |
|||
|
|||
/** |
|||
* 将华氏度转换为摄氏度 |
|||
* @param f 华氏温度 |
|||
* @return 对应的摄氏温度 |
|||
*/ |
|||
public static double fahrenheitToCelsius(double f) { |
|||
return (f - 32.0) * 5.0 / 9.0; |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
Scanner scanner = new Scanner(System.in); |
|||
|
|||
// 提示用户输入,格式示例:"36.6 C" 或 "97 F" |
|||
System.out.print("请输入要转换的温度与单位(例如 36.6 C 或 97 F):"); |
|||
String input = scanner.nextLine().trim(); |
|||
|
|||
if (input.isEmpty()) { |
|||
System.out.println("输入为空,程序退出。"); |
|||
scanner.close(); |
|||
return; |
|||
} |
|||
|
|||
String[] parts = input.split("\\s+"); |
|||
|
|||
try { |
|||
// 解析数值和单位 |
|||
double value = Double.parseDouble(parts[0]); |
|||
String unit = parts.length > 1 ? parts[1].toUpperCase() : "C"; |
|||
|
|||
if (unit.startsWith("C")) { |
|||
// 从摄氏度转换为华氏度 |
|||
double f = celsiusToFahrenheit(value); |
|||
System.out.printf("%.2f °C = %.2f °F%n", value, f); |
|||
} else if (unit.startsWith("F")) { |
|||
// 从华氏度转换为摄氏度 |
|||
double c = fahrenheitToCelsius(value); |
|||
System.out.printf("%.2f °F = %.2f °C%n", value, c); |
|||
} else { |
|||
System.out.println("未知单位,请使用 C 或 F。"); |
|||
} |
|||
|
|||
} catch (NumberFormatException e) { |
|||
System.out.println("输入解析失败,请按示例输入数值与单位,例如:36.6 C"); |
|||
} catch (Exception e) { |
|||
System.out.println("发生错误:" + e.getMessage()); |
|||
} finally { |
|||
scanner.close(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 import java.io.BufferedReader;\ |
|||
import java.io.FileReader;\ |
|||
import java.io.File;\ |
|||
import java.util.Scanner;\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u28201 \u24230 \u36716 \u25442 \u22120 \u31243 \u24207 \u65288 Java\u65289 \ |
|||
* \uc0\u31561 \u25928 \u31227 \u26893 Python\u28201 \u24230 \u36716 \u25442 \u31243 \u24207 \u65292 \u25903 \u25345 \u25668 \u27663 /\u21326 \u27663 \u20114 \u36716 \ |
|||
* \uc0\u39069 \u22806 \u25903 \u25345 \u65306 \u21629 \u20196 \u34892 \u21442 \u25968 \u27169 \u24335 \u12289 \u25991 \u20214 \u25209 \u37327 \u36716 \u25442 \u27169 \u24335 \ |
|||
*/\ |
|||
public class TemperatureConverter \{\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u25668 \u27663 \u24230 \u36716 \u25442 \u20026 \u21326 \u27663 \u24230 \ |
|||
* @param c \uc0\u36755 \u20837 \u30340 \u25668 \u27663 \u28201 \u24230 \u65292 \u25968 \u25454 \u31867 \u22411 \u20026 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
* @return \uc0\u36716 \u25442 \u21518 \u30340 \u21326 \u27663 \u28201 \u24230 \u65292 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
*/\ |
|||
public static double celsiusToFahrenheit(double c) \{\ |
|||
return c * 9.0 / 5.0 + 32.0;\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u21326 \u27663 \u24230 \u36716 \u25442 \u20026 \u25668 \u27663 \u24230 \ |
|||
* @param f \uc0\u36755 \u20837 \u30340 \u21326 \u27663 \u28201 \u24230 \u65292 \u25968 \u25454 \u31867 \u22411 \u20026 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
* @return \uc0\u36716 \u25442 \u21518 \u30340 \u25668 \u27663 \u28201 \u24230 \u65292 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
*/\ |
|||
public static double fahrenheitToCelsius(double f) \{\ |
|||
return (f - 32.0) * 5.0 / 9.0;\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u25209 \u37327 \u36716 \u25442 \u65306 \u20174 \u25991 \u20214 \u35835 \u21462 \u22810 \u34892 \u28201 \u24230 \u25968 \u25454 \u24182 \u23436 \u25104 \u36716 \u25442 \u65288 \u21152 \u20998 \u39033 \u65289 \ |
|||
* @param filename \uc0\u23384 \u20648 \u28201 \u24230 \u25968 \u25454 \u30340 \u25991 \u20214 \u21517 \u65292 \u23383 \u31526 \u20018 \u31867 \u22411 \ |
|||
*/\ |
|||
public static void batchConvert(String filename) \{\ |
|||
try (BufferedReader br = new BufferedReader(new FileReader(filename))) \{\ |
|||
String line;\ |
|||
while ((line = br.readLine()) != null) \{\ |
|||
convertAndPrint(line.trim()); // \uc0\u35843 \u29992 \u32479 \u19968 \u36716 \u25442 \u26041 \u27861 \ |
|||
\}\ |
|||
\} catch (Exception e) \{\ |
|||
System.out.println("\uc0\u25991 \u20214 \u35835 \u21462 \u22833 \u36133 \u65306 " + e.getMessage());\ |
|||
\}\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u32479 \u19968 \u36716 \u25442 \u36923 \u36753 \u65306 \u35299 \u26512 \u36755 \u20837 \u24182 \u36755 \u20986 \u32467 \u26524 \u65288 \u22797 \u29992 \u26680 \u24515 \u36923 \u36753 \u65289 \ |
|||
* @param input \uc0\u24453 \u35299 \u26512 \u30340 \u28201 \u24230 \u19982 \u21333 \u20301 \u23383 \u31526 \u20018 \u65292 \u22914 "36.6 C"\ |
|||
*/\ |
|||
public static void convertAndPrint(String input) \{\ |
|||
if (input.isEmpty()) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u20026 \u31354 \u65292 \u36339 \u36807 \u36716 \u25442 \u12290 ");\ |
|||
return;\ |
|||
\}\ |
|||
String[] parts = input.split("\\\\s+");\ |
|||
try \{\ |
|||
double value = Double.parseDouble(parts[0]);\ |
|||
// \uc0\u19982 \u21407 Python\u36923 \u36753 \u19968 \u33268 \u65306 \u26410 \u36755 \u20837 \u21333 \u20301 \u26102 \u40664 \u35748 \u25353 \u25668 \u27663 \u24230 \u22788 \u29702 \ |
|||
String unit = parts.length > 1 ? parts[1].toUpperCase() : "C";\ |
|||
\ |
|||
if (unit.startsWith("C")) \{\ |
|||
double f = celsiusToFahrenheit(value);\ |
|||
System.out.printf("%.2f \'b0C = %.2f \'b0F%n", value, f);\ |
|||
\} else if (unit.startsWith("F")) \{\ |
|||
double c = fahrenheitToCelsius(value);\ |
|||
System.out.printf("%.2f \'b0F = %.2f \'b0C%n", value, c);\ |
|||
\} else \{\ |
|||
System.out.println("\uc0\u26410 \u30693 \u21333 \u20301 \u65292 \u35831 \u20351 \u29992 C \u25110 F\u12290 ");\ |
|||
\}\ |
|||
\} catch (Exception e) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u35299 \u26512 \u22833 \u36133 \u65292 \u35831 \u25353 \u31034 \u20363 \u36755 \u20837 \u65288 \u22914 36.6 C\u65289 \u12290 ");\ |
|||
\}\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u31243 \u24207 \u20027 \u20837 \u21475 \u65306 \u25972 \u21512 \u19977 \u31181 \u36816 \u34892 \u27169 \u24335 \ |
|||
* @param args \uc0\u21629 \u20196 \u34892 \u21442 \u25968 \u65292 \u25903 \u25345 \u26080 \u21442 \u25968 \u65288 \u20132 \u20114 \u24335 \u65289 \u12289 \u21442 \u25968 \u20026 \u28201 \u24230 \u21333 \u20301 \u65288 \u21629 \u20196 \u34892 \u27169 \u24335 \u65289 \u12289 \u21442 \u25968 \u20026 \u25991 \u20214 \u21517 \u65288 \u25209 \u37327 \u27169 \u24335 \u65289 \ |
|||
*/\ |
|||
public static void main(String[] args) \{\ |
|||
// \uc0\u27169 \u24335 1\u65306 \u25209 \u37327 \u25991 \u20214 \u36716 \u25442 \u65288 \u21152 \u20998 \u39033 \u65289 - \u20165 1\u20010 \u21442 \u25968 \u19988 \u20026 \u25991 \u20214 \u26102 \u35302 \u21457 \ |
|||
if (args.length == 1 && new File(args[0]).exists()) \{\ |
|||
batchConvert(args[0]);\ |
|||
return;\ |
|||
\}\ |
|||
\ |
|||
Scanner scanner = new Scanner(System.in);\ |
|||
String input;\ |
|||
\ |
|||
// \uc0\u27169 \u24335 2\u65306 \u21629 \u20196 \u34892 \u21442 \u25968 \u27169 \u24335 \u65288 \u21152 \u20998 \u39033 \u65289 - \u22810 \u21442 \u25968 \u26102 \u30452 \u25509 \u25340 \u25509 \u20026 \u36755 \u20837 \ |
|||
if (args.length > 0) \{\ |
|||
input = String.join(" ", args);\ |
|||
\} else \{\ |
|||
// \uc0\u27169 \u24335 3\u65306 \u20132 \u20114 \u24335 \u36755 \u20837 \u65288 \u21407 Python\u26680 \u24515 \u21151 \u33021 \u65292 \u24517 \u20570 \u65289 \ |
|||
System.out.print("\uc0\u35831 \u36755 \u20837 \u35201 \u36716 \u25442 \u30340 \u28201 \u24230 \u19982 \u21333 \u20301 \u65288 \u20363 \u22914 36.6 C \u25110 97 F\u65289 \u65306 ");\ |
|||
input = scanner.nextLine().trim();\ |
|||
\}\ |
|||
\ |
|||
convertAndPrint(input); // \uc0\u25191 \u34892 \u26680 \u24515 \u36716 \u25442 \ |
|||
scanner.close();\ |
|||
\}\ |
|||
\}} |
|||
@ -0,0 +1,43 @@ |
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 PingFangSC-Regular;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 AI |
|||
\f1 \'d0\'ad\'d6\'fa\'bf\'aa\'b7\'a2\'ce\'c2\'b6\'c8\'d7\'aa\'bb\'bb\'b3\'cc\'d0\'f2\'bc\'c7\'c2\'bc |
|||
\f0 \ |
|||
\ |
|||
|
|||
\f1 \'d4\'da\'bf\'aa\'b7\'a2\'ce\'c2\'b6\'c8\'d7\'aa\'bb\'bb\'b3\'cc\'d0\'f2\'ca\'b1\'a3\'ac |
|||
\f0 AI |
|||
\f1 \'cc\'e1\'b9\'a9\'c1\'cb\'c7\'e5\'ce\'fa\'a1\'a2\'b8\'df\'d0\'a7\'b5\'c4\'d6\'a7\'b3\'d6\'a3\'ac\'d6\'f7\'d2\'aa\'cc\'e5\'cf\'d6\'d4\'da\'d2\'d4\'cf\'c2\'bc\'b8\'b8\'f6\'b7\'bd\'c3\'e6\'a3\'ba |
|||
\f0 \ |
|||
\ |
|||
1.\'a0 |
|||
\f1 \'d0\'e8\'c7\'f3\'ca\'e1\'c0\'ed\'a3\'ba\'c3\'f7\'c8\'b7\'b3\'cc\'d0\'f2\'d0\'e8\'d6\'a7\'b3\'d6\'c9\'e3\'ca\'cf |
|||
\f0 / |
|||
\f1 \'bb\'aa\'ca\'cf\'bb\'a5\'d7\'aa\'a1\'a2\'c3\'fc\'c1\'ee\'d0\'d0\'b2\'ce\'ca\'fd\'ba\'cd\'c5\'fa\'c1\'bf\'ce\'c4\'bc\'fe\'d7\'aa\'bb\'bb\'c8\'fd\'d6\'d6\'c4\'a3\'ca\'bd\'a3\'ac\'b2\'a2\'b9\'e6\'bb\'ae\'c1\'cb\'c4\'a3\'bf\'e9\'bb\'af\'bd\'e1\'b9\'b9\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
2.\'a0 |
|||
\f1 \'b4\'fa\'c2\'eb\'c9\'fa\'b3\'c9\'a3\'ba\'c9\'fa\'b3\'c9\'c1\'cb\'cd\'ea\'d5\'fb\'b5\'c4 |
|||
\f0 Java |
|||
\f1 \'b4\'fa\'c2\'eb\'bf\'f2\'bc\'dc\'a3\'ac\'b0\'fc\'c0\'a8\'ba\'cb\'d0\'c4\'d7\'aa\'bb\'bb\'ba\'af\'ca\'fd\'a1\'a2\'cd\'b3\'d2\'bb\'bd\'e2\'ce\'f6\'c2\'df\'bc\'ad\'ba\'cd\'c5\'fa\'c1\'bf\'b4\'a6\'c0\'ed\'c4\'a3\'bf\'e9\'a3\'ac\'b2\'a2\'bc\'d3\'c8\'eb\'c1\'cb\'d2\'ec\'b3\'a3\'b4\'a6\'c0\'ed\'a3\'ac\'cc\'e1\'c9\'fd\'c1\'cb\'b3\'cc\'d0\'f2\'bd\'a1\'d7\'b3\'d0\'d4\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
3.\'a0 |
|||
\f1 \'bb\'b7\'be\'b3\'d6\'b8\'b5\'bc\'a3\'ba\'d5\'eb\'b6\'d4 |
|||
\f0 Mac |
|||
\f1 \'bb\'b7\'be\'b3\'a3\'ac\'cc\'e1\'b9\'a9\'c1\'cb\'b1\'e0\'d2\'eb\'a1\'a2\'d4\'cb\'d0\'d0\'ba\'cd |
|||
\f0 Git |
|||
\f1 \'cc\'e1\'bd\'bb\'b5\'c4\'b7\'d6\'b2\'bd\'b2\'d9\'d7\'f7\'a3\'ac\'bd\'e2\'be\'f6\'c1\'cb\'ce\'c4\'bc\'fe\'c2\'b7\'be\'b6\'a1\'a2\'d6\'d5\'b6\'cb\'b1\'a8\'b4\'ed\'b5\'c8\'ce\'ca\'cc\'e2\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
4.\'a0 |
|||
\f1 \'b2\'d6\'bf\'e2\'d3\'c5\'bb\'af\'a3\'ba\'bd\'a8\'d2\'e9\'cd\'a8\'b9\'fd |
|||
\f0 \'a0.gitignore\'a0 |
|||
\f1 \'ce\'c4\'bc\'fe\'b9\'e6\'b7\'b6\'cc\'e1\'bd\'bb\'a3\'ac\'b1\'dc\'c3\'e2\'ce\'de\'b9\'d8\'ce\'c4\'bc\'fe\'b8\'c9\'c8\'c5\'a3\'ac\'b2\'a2\'d6\'b8\'b5\'bc\'c8\'e7\'ba\'ce\'d4\'da |
|||
\f0 README |
|||
\f1 \'d6\'d0\'d5\'b9\'ca\'be\'b4\'fa\'c2\'eb\'ba\'cd\'d4\'cb\'d0\'d0\'bd\'d8\'cd\'bc\'a1\'a3} |
|||
@ -0,0 +1,43 @@ |
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 PingFangSC-Regular;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 AI |
|||
\f1 \'d0\'ad\'d6\'fa\'bf\'aa\'b7\'a2\'ce\'c2\'b6\'c8\'d7\'aa\'bb\'bb\'b3\'cc\'d0\'f2\'bc\'c7\'c2\'bc |
|||
\f0 \ |
|||
\ |
|||
|
|||
\f1 \'d4\'da\'bf\'aa\'b7\'a2\'ce\'c2\'b6\'c8\'d7\'aa\'bb\'bb\'b3\'cc\'d0\'f2\'ca\'b1\'a3\'ac |
|||
\f0 AI |
|||
\f1 \'cc\'e1\'b9\'a9\'c1\'cb\'c7\'e5\'ce\'fa\'a1\'a2\'b8\'df\'d0\'a7\'b5\'c4\'d6\'a7\'b3\'d6\'a3\'ac\'d6\'f7\'d2\'aa\'cc\'e5\'cf\'d6\'d4\'da\'d2\'d4\'cf\'c2\'bc\'b8\'b8\'f6\'b7\'bd\'c3\'e6\'a3\'ba |
|||
\f0 \ |
|||
\ |
|||
1.\'a0 |
|||
\f1 \'d0\'e8\'c7\'f3\'ca\'e1\'c0\'ed\'a3\'ba\'c3\'f7\'c8\'b7\'b3\'cc\'d0\'f2\'d0\'e8\'d6\'a7\'b3\'d6\'c9\'e3\'ca\'cf |
|||
\f0 / |
|||
\f1 \'bb\'aa\'ca\'cf\'bb\'a5\'d7\'aa\'a1\'a2\'c3\'fc\'c1\'ee\'d0\'d0\'b2\'ce\'ca\'fd\'ba\'cd\'c5\'fa\'c1\'bf\'ce\'c4\'bc\'fe\'d7\'aa\'bb\'bb\'c8\'fd\'d6\'d6\'c4\'a3\'ca\'bd\'a3\'ac\'b2\'a2\'b9\'e6\'bb\'ae\'c1\'cb\'c4\'a3\'bf\'e9\'bb\'af\'bd\'e1\'b9\'b9\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
2.\'a0 |
|||
\f1 \'b4\'fa\'c2\'eb\'c9\'fa\'b3\'c9\'a3\'ba\'c9\'fa\'b3\'c9\'c1\'cb\'cd\'ea\'d5\'fb\'b5\'c4 |
|||
\f0 Java |
|||
\f1 \'b4\'fa\'c2\'eb\'bf\'f2\'bc\'dc\'a3\'ac\'b0\'fc\'c0\'a8\'ba\'cb\'d0\'c4\'d7\'aa\'bb\'bb\'ba\'af\'ca\'fd\'a1\'a2\'cd\'b3\'d2\'bb\'bd\'e2\'ce\'f6\'c2\'df\'bc\'ad\'ba\'cd\'c5\'fa\'c1\'bf\'b4\'a6\'c0\'ed\'c4\'a3\'bf\'e9\'a3\'ac\'b2\'a2\'bc\'d3\'c8\'eb\'c1\'cb\'d2\'ec\'b3\'a3\'b4\'a6\'c0\'ed\'a3\'ac\'cc\'e1\'c9\'fd\'c1\'cb\'b3\'cc\'d0\'f2\'bd\'a1\'d7\'b3\'d0\'d4\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
3.\'a0 |
|||
\f1 \'bb\'b7\'be\'b3\'d6\'b8\'b5\'bc\'a3\'ba\'d5\'eb\'b6\'d4 |
|||
\f0 Mac |
|||
\f1 \'bb\'b7\'be\'b3\'a3\'ac\'cc\'e1\'b9\'a9\'c1\'cb\'b1\'e0\'d2\'eb\'a1\'a2\'d4\'cb\'d0\'d0\'ba\'cd |
|||
\f0 Git |
|||
\f1 \'cc\'e1\'bd\'bb\'b5\'c4\'b7\'d6\'b2\'bd\'b2\'d9\'d7\'f7\'a3\'ac\'bd\'e2\'be\'f6\'c1\'cb\'ce\'c4\'bc\'fe\'c2\'b7\'be\'b6\'a1\'a2\'d6\'d5\'b6\'cb\'b1\'a8\'b4\'ed\'b5\'c8\'ce\'ca\'cc\'e2\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
4.\'a0 |
|||
\f1 \'b2\'d6\'bf\'e2\'d3\'c5\'bb\'af\'a3\'ba\'bd\'a8\'d2\'e9\'cd\'a8\'b9\'fd |
|||
\f0 \'a0.gitignore\'a0 |
|||
\f1 \'ce\'c4\'bc\'fe\'b9\'e6\'b7\'b6\'cc\'e1\'bd\'bb\'a3\'ac\'b1\'dc\'c3\'e2\'ce\'de\'b9\'d8\'ce\'c4\'bc\'fe\'b8\'c9\'c8\'c5\'a3\'ac\'b2\'a2\'d6\'b8\'b5\'bc\'c8\'e7\'ba\'ce\'d4\'da |
|||
\f0 README |
|||
\f1 \'d6\'d0\'d5\'b9\'ca\'be\'b4\'fa\'c2\'eb\'ba\'cd\'d4\'cb\'d0\'d0\'bd\'d8\'cd\'bc\'a1\'a3} |
|||
@ -0,0 +1,5 @@ |
|||
public class HelloWorld { |
|||
public static void main(String[] args) { |
|||
System.out.println("Hello World!"); |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<groupId>com.example</groupId> |
|||
<artifactId>datacollect-cli</artifactId> |
|||
<version>0.1.0</version> |
|||
<properties> |
|||
<maven.compiler.source>11</maven.compiler.source> |
|||
<maven.compiler.target>11</maven.compiler.target> |
|||
</properties> |
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-compiler-plugin</artifactId> |
|||
<version>3.8.1</version> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-assembly-plugin</artifactId> |
|||
<version>3.3.0</version> |
|||
<configuration> |
|||
<archive> |
|||
<manifest> |
|||
<mainClass>com.example.datacollect.Main</mainClass> |
|||
</manifest> |
|||
</archive> |
|||
<descriptorRefs> |
|||
<descriptorRef>jar-with-dependencies</descriptorRef> |
|||
</descriptorRefs> |
|||
</configuration> |
|||
<executions> |
|||
<execution> |
|||
<id>make-assembly</id> |
|||
<phase>package</phase> |
|||
<goals> |
|||
<goal>single</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
|||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 506 KiB |
|
Before Width: | Height: | Size: 501 KiB |
|
Before Width: | Height: | Size: 502 KiB |
|
Before Width: | Height: | Size: 502 KiB |
|
Before Width: | Height: | Size: 673 KiB |
|
Before Width: | Height: | Size: 619 KiB |
@ -1,60 +0,0 @@ |
|||
package com.example.datacollect; // 声明包名,与项目结构匹配
|
|||
|
|||
import java.util.Date; // 导入日期类,因为要用到Date类型
|
|||
|
|||
// Model层:只封装数据,无业务逻辑
|
|||
public class Article { |
|||
// 原有字段
|
|||
private String title; // 文章标题
|
|||
private String content; // 文章内容
|
|||
// 新增字段
|
|||
private String author; // 文章作者
|
|||
private Date publishDate; // 发布日期
|
|||
|
|||
// 1. 无参构造器(方便创建空对象)
|
|||
public Article() { |
|||
} |
|||
|
|||
// 2. 全参构造器(方便一次性赋值创建对象)
|
|||
public Article(String title, String content, String author, Date publishDate) { |
|||
this.title = title; |
|||
this.content = content; |
|||
this.author = author; |
|||
this.publishDate = publishDate; |
|||
} |
|||
|
|||
// 3. Getter/Setter方法(私有字段只能通过这些方法读写)
|
|||
public String getTitle() { |
|||
return title; |
|||
} |
|||
|
|||
public void setTitle(String title) { |
|||
this.title = title; |
|||
} |
|||
|
|||
public String getContent() { |
|||
return content; |
|||
} |
|||
|
|||
public void setContent(String content) { |
|||
this.content = content; |
|||
} |
|||
|
|||
// 新增字段的get/set
|
|||
public String getAuthor() { |
|||
return author; |
|||
} |
|||
|
|||
public void setAuthor(String author) { |
|||
this.author = author; |
|||
} |
|||
|
|||
public Date getPublishDate() { |
|||
return publishDate; |
|||
} |
|||
|
|||
public void setPublishDate(Date publishDate) { |
|||
this.publishDate = publishDate; |
|||
} |
|||
} |
|||
|
|||
@ -1,52 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
import java.util.Scanner; |
|||
|
|||
// Controller层:处理用户输入、调用工具类/模型,是程序的入口
|
|||
public class CommandController { |
|||
public static void main(String[] args) { |
|||
// 在CommandController的main方法开头
|
|||
System.out.println("===== 暗色主题模式 ====="); |
|||
System.out.println("背景色:" + ThemeConfig.BACKGROUND_COLOR); |
|||
System.out.println("文字色:" + ThemeConfig.TEXT_COLOR); |
|||
// 1. 创建扫描器,接收用户输入
|
|||
Scanner scanner = new Scanner(System.in); |
|||
System.out.println("===== 命令行工具 ====="); |
|||
System.out.println("指令说明:c/crawl=执行爬虫 | exit=退出 | 其他=未知命令"); |
|||
|
|||
// 2. 循环接收命令(直到输入exit)
|
|||
while (true) { |
|||
System.out.print("\n请输入命令:"); |
|||
String inputCommand = scanner.nextLine().trim(); // 去除首尾空格
|
|||
|
|||
// 3. 把命令记录到历史(调用HistoryCommand的方法)
|
|||
HistoryCommand.addCommand(inputCommand); |
|||
// 在CommandController的while循环中,新增else if分支
|
|||
if (inputCommand.startsWith("url ")) { // 比如输入:url https://www.baidu.com
|
|||
// 截取URL部分(去掉"url "前缀)
|
|||
String url = inputCommand.substring(4).trim(); |
|||
if (UrlValidator.isValidUrl(url)) { |
|||
System.out.println("✅ URL格式合法:" + url); |
|||
} else { |
|||
System.out.println("❌ URL格式非法:" + url); |
|||
} |
|||
} |
|||
|
|||
// 4. 命令别名:c 等价于 crawl
|
|||
if (inputCommand.equals("c") || inputCommand.equals("crawl")) { |
|||
System.out.println("✅ 执行爬虫命令..."); |
|||
// 这里可以后续扩展:调用爬虫逻辑,比如爬取文章并封装到Article
|
|||
} else if (inputCommand.equals("exit")) { |
|||
System.out.println("❌ 退出程序,历史命令如下:"); |
|||
// 打印所有历史命令
|
|||
System.out.println(HistoryCommand.getCommandHistory()); |
|||
break; // 退出循环,结束程序
|
|||
} else { |
|||
System.out.println("❓ 未知命令,请重新输入!"); |
|||
} |
|||
} |
|||
|
|||
// 5. 关闭扫描器(释放资源)
|
|||
scanner.close(); |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
// 工具类:专门记录命令历史,属于MVC的辅助层
|
|||
public class HistoryCommand { |
|||
// 1. 私有化List,避免外部直接修改(安全)
|
|||
// static:整个程序只有一份,所有地方共用这个历史列表
|
|||
private static List<String> commandHistory = new ArrayList<>(); |
|||
|
|||
// 2. 添加命令到历史(外部调用这个方法记录命令)
|
|||
public static void addCommand(String command) { |
|||
commandHistory.add(command); |
|||
} |
|||
|
|||
// 3. 获取所有历史命令(返回副本,避免原列表被外部篡改)
|
|||
public static List<String> getCommandHistory() { |
|||
return new ArrayList<>(commandHistory); // 返回副本,原列表不会被改
|
|||
} |
|||
|
|||
// 4. 清空历史命令(可选,方便测试)
|
|||
public static void clearHistory() { |
|||
commandHistory.clear(); |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
import com.example.datacollect.controller.CrawlerController; |
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class Main { |
|||
|
|||
public static void main(String[] args) { |
|||
ConsoleView view = new ConsoleView(); |
|||
List<Article> articles = new ArrayList<>(); |
|||
CrawlerController controller = new CrawlerController(view, articles); |
|||
|
|||
view.printSuccess("Welcome to CLI Crawler (w9_1)! Type help for commands."); |
|||
while (true) { |
|||
controller.handle(view.readLine()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
// 常量类:存储主题配置(只改这里的常量,整个程序的主题就变了)
|
|||
public class ThemeConfig { |
|||
// 暗色主题:修改BACKGROUND_COLOR常量(这就是“修改一处常量”)
|
|||
public static final String BACKGROUND_COLOR = "#1E1E1E"; // 暗色背景(原亮色是#FFFFFF)
|
|||
public static final String TEXT_COLOR = "#FFFFFF"; // 文字白色(适配暗色背景)
|
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
import java.util.regex.Pattern; |
|||
|
|||
// 工具类:专门验证URL格式
|
|||
public class UrlValidator { |
|||
// 1. 定义URL正则表达式(简化版,能匹配http/https开头的网址)
|
|||
private static final String URL_REGEX = "^(https?://)?([a-zA-Z0-9_-]+\\.)+[a-zA-Z]{2,6}(/.*)?$"; |
|||
// 2. 编译正则表达式(提升匹配效率)
|
|||
private static final Pattern URL_PATTERN = Pattern.compile(URL_REGEX); |
|||
|
|||
// 3. 公开方法:验证URL是否合法,返回true/false
|
|||
public static boolean isValidUrl(String url) { |
|||
// 先判断URL是否为空
|
|||
if (url == null || url.trim().isEmpty()) { |
|||
return false; |
|||
} |
|||
// 用正则匹配URL
|
|||
return URL_PATTERN.matcher(url.trim()).matches(); |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
package com.example.datacollect.command; |
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import java.util.List; |
|||
|
|||
public interface Command { |
|||
String getName(); |
|||
void execute(String[] args, List<Article> articles); |
|||
} |
|||
@ -1,27 +0,0 @@ |
|||
package com.example.datacollect.command; |
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import java.util.List; |
|||
|
|||
public class CrawlCommand implements Command { |
|||
private final ConsoleView view; |
|||
|
|||
public CrawlCommand(ConsoleView view) { |
|||
this.view = view; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "crawl"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute(String[] args, List<Article> articles) { |
|||
if (args.length < 2) { |
|||
view.printError("Usage: crawl <url>"); |
|||
return; |
|||
} |
|||
view.printInfo("Stub: would crawl " + args[1]); |
|||
} |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
package com.example.datacollect.command; |
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import java.util.List; |
|||
|
|||
public class ExitCommand implements Command { |
|||
private final ConsoleView view; |
|||
|
|||
public ExitCommand(ConsoleView view) { |
|||
this.view = view; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "exit"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.printSuccess("Bye!"); |
|||
System.exit(0); |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
package com.example.datacollect.command; |
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import java.util.List; |
|||
|
|||
public class HelpCommand implements Command { |
|||
private final ConsoleView view; |
|||
|
|||
public HelpCommand(ConsoleView view) { |
|||
this.view = view; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "help"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.printInfo("Commands: crawl <url>, list, help, exit"); |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
package com.example.datacollect.command; |
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import java.util.List; |
|||
|
|||
public class ListCommand implements Command { |
|||
private final ConsoleView view; |
|||
|
|||
public ListCommand(ConsoleView view) { |
|||
this.view = view; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "list"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.display(articles); |
|||
} |
|||
} |
|||
@ -1,47 +0,0 @@ |
|||
package com.example.datacollect.controller; |
|||
|
|||
import com.example.datacollect.command.Command; |
|||
import com.example.datacollect.command.CrawlCommand; |
|||
import com.example.datacollect.command.ExitCommand; |
|||
import com.example.datacollect.command.HelpCommand; |
|||
import com.example.datacollect.command.ListCommand; |
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class CrawlerController { |
|||
private final Map<String, Command> commands = new HashMap<>(); |
|||
private final ConsoleView view; |
|||
private final List<Article> articles; |
|||
|
|||
public CrawlerController(ConsoleView view, List<Article> articles) { |
|||
this.view = view; |
|||
this.articles = articles; |
|||
register(new HelpCommand(view)); |
|||
register(new ListCommand(view)); |
|||
register(new CrawlCommand(view)); |
|||
register(new ExitCommand(view)); |
|||
} |
|||
|
|||
private void register(Command command) { |
|||
commands.put(command.getName(), command); |
|||
} |
|||
|
|||
public void handle(String input) { |
|||
String text = input == null ? "" : input.trim(); |
|||
if (text.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
String[] args = text.split("\\s+"); |
|||
String cmdName = args[0].toLowerCase(); |
|||
Command command = commands.get(cmdName); |
|||
if (command == null) { |
|||
view.printError("Unknown command: " + cmdName); |
|||
return; |
|||
} |
|||
command.execute(args, articles); |
|||
} |
|||
} |
|||
@ -1,45 +0,0 @@ |
|||
package com.example.datacollect.model; |
|||
|
|||
public class Article { |
|||
private String title; |
|||
private String url; |
|||
private String content; |
|||
|
|||
public Article(String title, String url, String content) { |
|||
this.title = title; |
|||
this.url = url; |
|||
this.content = content; |
|||
} |
|||
|
|||
public String getTitle() { |
|||
return title; |
|||
} |
|||
|
|||
public void setTitle(String title) { |
|||
this.title = title; |
|||
} |
|||
|
|||
public String getUrl() { |
|||
return url; |
|||
} |
|||
|
|||
public void setUrl(String url) { |
|||
this.url = url; |
|||
} |
|||
|
|||
public String getContent() { |
|||
return content; |
|||
} |
|||
|
|||
public void setContent(String content) { |
|||
this.content = content; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Article{" |
|||
+ "title='" + title + '\'' |
|||
+ ", url='" + url + '\'' |
|||
+ '}'; |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
package com.example.datacollect.view; |
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import java.util.List; |
|||
import java.util.Scanner; |
|||
|
|||
public class ConsoleView { |
|||
private static final String ANSI_RESET = "\u001B[0m"; |
|||
private static final String ANSI_GREEN = "\u001B[32m"; |
|||
private static final String ANSI_RED = "\u001B[31m"; |
|||
private static final String ANSI_BLUE = "\u001B[34m"; |
|||
|
|||
private final Scanner scanner = new Scanner(System.in); |
|||
|
|||
public String readLine() { |
|||
System.out.print("> "); |
|||
return scanner.nextLine(); |
|||
} |
|||
|
|||
public void printSuccess(String msg) { |
|||
System.out.println(ANSI_GREEN + msg + ANSI_RESET); |
|||
} |
|||
|
|||
public void printError(String msg) { |
|||
System.out.println(ANSI_RED + msg + ANSI_RESET); |
|||
} |
|||
|
|||
public void printInfo(String msg) { |
|||
System.out.println(ANSI_BLUE + msg + ANSI_RESET); |
|||
} |
|||
|
|||
public void display(List<Article> articles) { |
|||
if (articles.isEmpty()) { |
|||
printInfo("暂无文章,请先执行 crawl。"); |
|||
return; |
|||
} |
|||
for (int i = 0; i < articles.size(); i++) { |
|||
Article a = articles.get(i); |
|||
System.out.println((i + 1) + ". " + a.getTitle() + " | " + a.getUrl()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,758 +0,0 @@ |
|||
--- |
|||
|
|||
# 教案:《高级程序设计》第9周——工程架构:从"写代码"到"造系统" |
|||
|
|||
| 项目 | 内容 | |
|||
|------|------| |
|||
| **课程名称** | 高级程序设计 | |
|||
| **周次** | 第9周 | |
|||
| **主题** | 工程架构——从"写代码"到"造系统" | |
|||
| **学时** | 2学时(90分钟) | |
|||
| **授课对象** | 具备Python基础、已完成Java面向对象特性学习的学生 | |
|||
| **教学环境** | JDK 17+、IntelliJ IDEA、Maven(模板) | |
|||
| **前情提要** | 本课程原计划使用JavaFX GUI,后根据教学反馈转向CLI + MVC + 爬虫工程化 | |
|||
|
|||
--- |
|||
|
|||
## 教学调整说明:为什么选择CLI而不是GUI? |
|||
|
|||
> **原计划**:JavaFX桌面应用 → **新计划**:CLI命令行应用 |
|||
|
|||
| 维度 | GUI (JavaFX) | CLI (命令行) | |
|||
|------|--------------|-------------| |
|||
| **学习重心** | 布局、控件、事件监听 | 架构、分层、命令路由 | |
|||
| **学生痛点** | "窗口点击"与后端能力无关 | 真正锻炼工程思维 | |
|||
| **AI辅助** | AI生成FXML,学生看不懂 | AI辅助重构架构 | |
|||
| **工程化** | 脱离真实后端开发场景 | 模拟真实服务器/大数据开发 | |
|||
| **核心转型** | "视觉装饰"优先 | "逻辑架构"优先 | |
|||
|
|||
**决策理由**: |
|||
1. **985学生需要的是工程思维**,不是拖控件 |
|||
2. **接口抽象**是弱项,CLI + MVC更能暴露这个问题 |
|||
3. **彩色终端**足够酷炫,且代码量可控 |
|||
|
|||
**更深层的教育价值**: |
|||
> 在GUI框架中,架构已被框架强制划定,学生只是"遵守规矩";而CLI世界里没有任何框架告诉你模型在哪、视图在哪——**当外部约束消失,内部的工程纪律才真正建立**。这正是本节课要传递的核心精神。 |
|||
|
|||
--- |
|||
|
|||
## 一、教学目标 |
|||
|
|||
| 目标维度 | 具体描述 | |
|||
|----------|----------| |
|||
| **知识掌握** | 理解MVC架构的职责划分及其演化脉络;掌握Maven项目结构与pom.xml基础;理解Command模式的路由原理。 | |
|||
| **工程实践** | 能搭建规范的Maven项目包结构;能实现基于Scanner的控制台交互;能用Command接口实现可扩展的命令路由;能识别架构中的"越权行为"。 | |
|||
| **思维转型** | 从"一个类写全部"转向"分层解耦";从"修改现有代码"转向"新增类实现功能";从"满足功能"转向"代码的工程洁癖"。 | |
|||
| **工具应用** | 利用AI辅助审查MVC职责越权;让AI扮演"架构审计师"检查分层是否清晰;理解AI生成代码中的架构缺陷。 | |
|||
|
|||
--- |
|||
|
|||
## 二、教学重点与难点 |
|||
|
|||
| 项目 | 内容 | 突破方法 | |
|||
|------|------|----------| |
|||
| **重点** | MVC三层职责划分、CLI交互实现、Command接口解耦、代码中的工程细节(常量、输出归属) | 以"新增命令需要改什么"为切入点,展示Command模式的优势;通过现场"代码找茬"强化细节意识 | |
|||
| **难点** | Controller不写业务逻辑、Command接口的多态实现、共享数据模型的设计缺陷识别 | 现场演示:增加一个命令只需新建类,无需修改Controller;暴露`List<Article>`共享引用的问题并预告解决方案 | |
|||
|
|||
--- |
|||
|
|||
## 三、教学过程设计(90分钟) |
|||
|
|||
| 环节 | 时间 | 教学内容 | 师生活动 | AI协同点 | |
|||
|------|------|----------|----------|----------| |
|||
| **1. 痛点引入:从脚本到工程的鸿沟** | 10' | 展示"意大利面"式爬虫代码,演示改一处需要动全身 | **教师演示**:现场展示一段混乱代码,让学生找问题 | 用AI分析代码耦合度 | |
|||
| **2. CLI vs GUI:架构选择的思考** | 10' | 对比两种方案的优缺点,解释为什么CLI更适合培养工程思维 | **教师讲解**:用对比表格说明选择CLI的理由 | — | |
|||
| **3. MVC分层设计** | 20' | 讲解Model/View/Controller三层职责,用"餐厅类比"强化理解,随后批判类比局限性 | **教师讲解**:配合架构图讲解三层交互,引导学生寻找类比破绽 | 用AI生成MVC职责对照表 | |
|||
| **4. Command模式:可扩展的命令路由** | 15' | 引入Command接口,解释"一个命令就是一个类" | **类比**:Command像酒店的服务部门,Controller是前台 | 让AI解释Command模式的多态原理 | |
|||
| **5. Maven模板与环境** | 5' | 直接使用提供的Maven模板,讲解目录结构 | **教师演示**:解压模板 → IDEA打开 → 运行 | — | |
|||
| **6. 三层代码落地** | 20' | **Model**:Article实体<br>**View**:ConsoleView(ANSI常量)<br>**Command接口**+实现<br>**Controller**:Map路由 | **教师演示**:分步写出代码,刻意埋入1~2个"越权细节"让学生找茬 | 学生用AI做"架构审计" | |
|||
| **7. 架构反思与展望** | 5' | 指出当前`List<Article>`共享引用的问题,预告W10策略模式与仓库层 | **师生互动**:你发现这个设计有什么风险? | 让AI分析共享可变状态的危害 | |
|||
| **8. 实践任务:空壳程序** | 5' | 搭建完整包结构,实现CLI循环 | 学生现场编码,教师巡视 | 完成后用AI检查包结构 | |
|||
| **9. 总结与过渡** | 5' | 本周实现了"骨架+命令可扩展",下周填入"灵魂"——解析器,并解决数据安全问题 | 总结Command模式优势,预告策略模式 | — | |
|||
|
|||
--- |
|||
|
|||
## 四、核心教学内容脚本 |
|||
|
|||
### 4.1 痛点引入:从脚本到工程的鸿沟(10分钟) |
|||
|
|||
**教师口播**: |
|||
> "同学们,前8周我们学的是Java语法,从变量到类,从继承到接口。但有一个问题:代码写完之后,怎么组织?" |
|||
> |
|||
> "来看这段代码——这是某个同学写的'爬虫',他一个人完成了一个'完整'的项目。" |
|||
|
|||
**展示"脚本式"代码**: |
|||
```java |
|||
public class Crawler { |
|||
public static void main(String[] args) { |
|||
System.out.print("请输入URL: "); |
|||
Scanner scanner = new Scanner(System.in); |
|||
String url = scanner.nextLine(); |
|||
|
|||
List titles = new ArrayList(); |
|||
try { |
|||
Document doc = Jsoup.connect(url).get(); |
|||
Elements elements = doc.select(".post-title"); |
|||
for (Element e : elements) { |
|||
String title = e.text(); |
|||
System.out.println("标题: " + title); |
|||
titles.add(title); |
|||
} |
|||
} catch (Exception ex) { |
|||
System.out.println("出错啦: " + ex.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**提问引导**: |
|||
1. "如果我想把标题保存到文件,要改哪里?" |
|||
2. "如果我想支持另一个网站,它的HTML结构不一样,要怎么办?" |
|||
3. "如果我想让输出变成彩色,要改哪里?" |
|||
|
|||
**痛点提炼**: |
|||
> "看到了吗?才60行代码,已经'牵一发而动全身'了。这就是一个'脚本'的宿命——功能全混在一起,改一个小需求,整个文件都要翻。" |
|||
> |
|||
> "这周我们要解决:**怎么让代码'改起来不疼'?**" |
|||
|
|||
--- |
|||
|
|||
### 4.2 CLI vs GUI:架构选择的思考(10分钟) |
|||
|
|||
**教师口播**: |
|||
> "既然要写一个'完整'的爬虫应用,我们有两个选择:图形界面(GUI)或命令行界面(CLI)。为什么我推荐CLI而不是GUI?" |
|||
|
|||
**对比表格** |
|||
|
|||
| 维度 | GUI (JavaFX) | CLI (命令行) | |
|||
|------|--------------|-------------| |
|||
| **代码量** | FXML + Controller + CSS,大量模板代码 | 纯Java,代码量可控 | |
|||
| **学习重心** | 布局、控件、事件监听 | 架构、分层、命令路由 | |
|||
| **后端能力** | 几乎无关 | 模拟真实服务器开发 | |
|||
| **可测试性** | 难(需要UI测试框架) | 易(直接测试Command类) | |
|||
| **工程思维** | 弱(关注视觉) | 强(关注逻辑) | |
|||
|
|||
**核心观点**: |
|||
> **CLI更需要MVC!** GUI有现成的事件系统(点击按钮→触发事件),而CLI只有字符流。**没有架构,分分钟写成脚本**。MVC在CLI里是"刚需",不是"装饰"。 |
|||
> |
|||
> **更深一层**:在GUI里,框架已经硬塞给你一套架构,你只是在填空;但在CLI里,所有结构都必须由你亲手搭建。**当外部约束消失,内部的工程纪律才真正开始建立**——这才是本节课的真正目的。 |
|||
|
|||
**CLI也能很酷**: |
|||
- ANSI彩色输出(红/绿/黄/蓝) |
|||
- 表格展示数据 |
|||
- 进度条动画 |
|||
- 模拟真实大数据开发场景 |
|||
|
|||
--- |
|||
|
|||
### 4.3 MVC分层设计(20分钟) |
|||
|
|||
#### 4.3.1 MVC的起源与演进 |
|||
|
|||
**教师口播**: |
|||
> "MVC不是新东西,它是1970年代为桌面应用设计的架构思想。但它的核心——'职责分离'——在任何软件里都适用。" |
|||
|
|||
| 年代 | 场景 | MVC的角色 | |
|||
|------|------|----------| |
|||
| 1970s | Smalltalk-72 GUI | 最早的用户界面架构 | |
|||
| 1990s | Web开发 (Struts) | 后端模板引擎 | |
|||
| 2000s | ASP.NET MVC | 现代Web框架 | |
|||
| 2020s | CLI + API | 解耦业务逻辑与表现层 | |
|||
|
|||
#### 4.3.2 从GUI到CLI的映射 |
|||
|
|||
| GUI组件 | CLI对应 | 说明 | |
|||
|--------|--------|------| |
|||
| 窗口/按钮 | 命令行输入 | **View = 用户交互** | |
|||
| 数据模型 | Article实体类 | **Model = 数据结构** | |
|||
| 事件监听 | Command路由 | **Controller = 调度** | |
|||
|
|||
#### 4.3.3 MVC三层职责 |
|||
|
|||
**架构图示**: |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────┐ |
|||
│ 入口 │ |
|||
│ (main方法) │ |
|||
└─────────────────┬───────────────────────┘ |
|||
│ |
|||
▼ |
|||
┌─────────────────────────────────────────┐ |
|||
│ Controller │ |
|||
│ - 接收命令(crawl, help, exit) │ |
|||
│ - 分发给对应的Command │ |
|||
│ 【口诀】:Controller不管"怎么做", │ |
|||
│ 只管"派给谁" │ |
|||
└─────────┬───────────────┬───────────────┘ |
|||
│ │ |
|||
▼ ▼ |
|||
┌─────────────────┐ ┌─────────────────┐ |
|||
│ Model │ │ View │ |
|||
│ - 数据实体 │ │ - 输入解析 │ |
|||
│ - 业务逻辑 │ │ - 输出格式化 │ |
|||
│ 【口诀】: │ │ 【口诀】: │ |
|||
│ Model管"数据" │ │ View管"呈现" │ |
|||
└─────────────────┘ └─────────────────┘ |
|||
``` |
|||
|
|||
**三层职责详解** |
|||
|
|||
| 层级 | 职责 | 典型代码 | 禁止做什么 | |
|||
|------|------|----------|------------| |
|||
| **Model** | 数据结构 + 业务逻辑 | `class Article { String title; String content; }` | 不能有`System.out.println`,不能有`Scanner` | |
|||
| **View** | 接收用户输入 + 格式化输出 | `class ConsoleView { String readInput(); void print(String); }` | 不能写爬虫逻辑,只做"传声筒" | |
|||
| **Controller** | 协调调度 | `class CrawlerController { void handle(String cmd) { ... } }` | 不能直接写业务细节,委托给Command | |
|||
|
|||
#### 4.3.4 类比强化:"餐厅类比" |
|||
|
|||
> "把MVC想象成一家餐厅: |
|||
> - **Model是后厨**:只管做菜(数据加工),不管谁来吃、怎么端 |
|||
> - **View是服务员**:只管端菜和收钱(输入输出),不管菜怎么做 |
|||
> - **Controller是前台**:只管把顾客的点单传给后厨,把做好的菜端给顾客 |
|||
> |
|||
> 如果后厨开始管'谁来吃饭',这餐厅就乱了。" |
|||
|
|||
#### 4.3.5 对"餐厅类比"的批判性思考(关键!) |
|||
|
|||
**教师导引**: |
|||
> "刚才的类比好理解吗?很好。但任何一个类比都有它的边界,如果把它当成真理,就会出问题。现在我们来给这个类比'找茬'。" |
|||
|
|||
**提问学生**: |
|||
1. "后厨真的完全不知道客人是谁吗?如果客人有忌口(比如不吃香菜),这个信息需不需要传到后厨?" |
|||
2. "服务员只是端菜吗?在真实餐厅里,服务员经常向后厨反馈'客人觉得今天的菜咸了',这属于View→Model的反向影响吗?" |
|||
3. "在这个类比里,我们把前台(Controller)和后厨(Model)的关系说成单向的。但实际上,后厨做完了菜,需要通知前台'菜好了',这不就是**观察者模式**吗?" |
|||
|
|||
**点明本质**: |
|||
> "实际MVC的数据流向常常是**双向**的:Controller调用Model的方法改变数据,Model变化后又通知View更新显示。只不过在本次CLI项目中,我们暂时使用'请求-响应'的单向简化模型——用户输入命令,系统处理,然后立即输出结果。这个简化版够用,但你要知道完整的MVC是更动态的。随着系统复杂,Model层需要一个专门的'仓库类'来管理数据,并通知视图刷新——这正是W10我们将要深入的内容。" |
|||
|
|||
#### 4.3.6 MVC的数据流向(本课程简化版) |
|||
|
|||
``` |
|||
CLI用户输入 |
|||
↓ |
|||
View(解析命令字符串) |
|||
↓ |
|||
Controller(找到对应Command) |
|||
↓ |
|||
Command.execute()(执行业务逻辑) |
|||
↓ |
|||
Model(Article数据,目前暂存于List) |
|||
↓ |
|||
View(display()展示数据) |
|||
↓ |
|||
CLI终端显示 |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### 4.4 Command模式:可扩展的命令路由(15分钟) |
|||
|
|||
**教师口播**: |
|||
> "现在引入一个设计模式——Command(命令)模式。它的核心思想是:**一个命令就是一个类**。" |
|||
|
|||
#### 4.4.1 为什么需要Command模式? |
|||
|
|||
**演示:增加一个命令的代价(switch-case版)** |
|||
```java |
|||
// 现状代码 |
|||
switch (cmd) { |
|||
case "crawl": handleCrawl(); break; |
|||
case "help": showHelp(); break; |
|||
// 如果要增加 list 命令? |
|||
// 1. 加 case "list" |
|||
// 2. 加 handleList() 方法 |
|||
// 3. 可能还要改其他地方... |
|||
} |
|||
``` |
|||
|
|||
**提问**: |
|||
- "如果我想增加10个命令,这个类要改多少次?" |
|||
- "如果我不小心删了一个case,整个程序还能跑吗?" |
|||
|
|||
**痛点提炼**: |
|||
> "每加一个功能,就要在这个类里戳一个洞。**这就是'肥控制器'陷阱**——所有的逻辑都堆在Controller里,它变成了新的'意大利面'。" |
|||
|
|||
#### 4.4.2 Command模式的四个要素 |
|||
|
|||
| 要素 | 角色 | 示例 | |
|||
|------|------|------| |
|||
| **Command接口** | 抽象的"订单" | `Command` 接口 | |
|||
| **ConcreteCommand** | 具体的订单 | `HelpCommand`、`CrawlCommand` | |
|||
| **Invoker** | 接单的前台 | `CrawlerController` | |
|||
| **Receiver** | 执行者 | `ConsoleView`、`ArticleRepository` | |
|||
|
|||
#### 4.4.3 Command接口定义 |
|||
|
|||
```java |
|||
// src/main/java/com/crawler/command/Command.java |
|||
package com.crawler.command; |
|||
|
|||
import com.crawler.model.Article; |
|||
import java.util.List; |
|||
|
|||
public interface Command { |
|||
String getName(); // 命令名,如 "crawl" |
|||
void execute(String[] args, List<Article> articles); // 执行逻辑 |
|||
} |
|||
``` |
|||
|
|||
#### 4.4.4 Controller的变革(从switch到Map) |
|||
|
|||
```java |
|||
// 修改后的Controller |
|||
public class CrawlerController { |
|||
private Map<String, Command> commands; // 用Map存命令 |
|||
private ConsoleView view; // 持有View以输出错误 |
|||
|
|||
public CrawlerController(ConsoleView view, List<Article> articles) { |
|||
this.view = view; |
|||
this.commands = new HashMap<>(); |
|||
// 增加命令无需改Controller代码,只需在这里注册 |
|||
commands.put("crawl", new CrawlCommand(view)); |
|||
commands.put("help", new HelpCommand(view)); |
|||
commands.put("list", new ListCommand(view)); |
|||
commands.put("exit", new ExitCommand(view)); |
|||
} |
|||
|
|||
public void handle(String input) { |
|||
if (input.isEmpty()) return; |
|||
String[] parts = input.split("\\s+"); |
|||
String cmd = parts[0].toLowerCase(); |
|||
|
|||
Command command = commands.get(cmd); |
|||
if (command == null) { |
|||
view.printError("Unknown command: " + cmd); // 通过View输出,而非直接System.out |
|||
return; |
|||
} |
|||
|
|||
// 执行命令,传入参数和文章列表 |
|||
command.execute(parts, articles); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**对比表格** |
|||
|
|||
| 维度 | switch-case | Command模式 | |
|||
|------|-------------|-------------| |
|||
| 增加命令 | 要改Controller | 新建一个类 | |
|||
| 多态体验 | 无 | execute()的多态调用 | |
|||
| 可测试性 | 难 | 每个Command可单独测试 | |
|||
| 代码量 | 少 | 多,但更清晰 | |
|||
|
|||
**类比强化**: |
|||
> "Command模式就像**酒店的客房服务**:每个服务(清理、送餐、按摩)都是一个独立的部门。前台(Controller)只负责接电话,然后把请求'派发'给对应的部门。部门自己知道怎么干活,不需要前台教。" |
|||
> |
|||
> "如果想新增一个服务,前台只需要'登记'一下,不需要把现有部门重新装修。" |
|||
|
|||
--- |
|||
|
|||
### 4.5 Maven模板与环境(5分钟) |
|||
|
|||
**教师口播**: |
|||
> "这周我们不发愁pom.xml配置。我已经把 Maven 模板准备好了,你们只需要解压、打开、运行。" |
|||
|
|||
**模板使用流程**: |
|||
``` |
|||
1. 解压 [my-crawler-template.zip] |
|||
2. 用 IDEA 打开文件夹 |
|||
3. 右键 pom.xml → Maven → Reload Project |
|||
4. 运行 App.java |
|||
``` |
|||
|
|||
**标准目录结构**: |
|||
``` |
|||
src/main/java/com/crawler/ |
|||
├── model/ |
|||
│ └── Article.java |
|||
├── view/ |
|||
│ └── ConsoleView.java |
|||
├── command/ |
|||
│ ├── Command.java (接口) |
|||
│ ├── CrawlCommand.java |
|||
│ ├── HelpCommand.java |
|||
│ ├── ListCommand.java |
|||
│ └── ExitCommand.java |
|||
└── controller/ |
|||
└── CrawlerController.java |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### 4.6 代码落地(20分钟) |
|||
|
|||
#### 4.6.1 Model层:Article实体 |
|||
|
|||
```java |
|||
// src/main/java/com/crawler/model/Article.java |
|||
package com.crawler.model; |
|||
|
|||
public class Article { |
|||
private String title; |
|||
private String url; |
|||
private String content; |
|||
|
|||
public Article(String title, String url, String content) { |
|||
this.title = title; |
|||
this.url = url; |
|||
this.content = content; |
|||
} |
|||
|
|||
public String getTitle() { return title; } |
|||
public void setTitle(String title) { this.title = title; } |
|||
public String getUrl() { return url; } |
|||
public void setUrl(String url) { this.url = url; } |
|||
public String getContent() { return content; } |
|||
public void setContent(String content) { this.content = content; } |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Article{title='" + title + "', url='" + url + "'}"; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### 4.6.2 View层:ANSI常量集中管理(工程细节!) |
|||
|
|||
```java |
|||
// src/main/java/com/crawler/view/ConsoleView.java |
|||
package com.crawler.view; |
|||
|
|||
import com.crawler.model.Article; |
|||
import java.util.List; |
|||
import java.util.Scanner; |
|||
|
|||
public class ConsoleView { |
|||
// ANSI颜色常量——集中管理,避免散落各处 |
|||
private static final String ANSI_GREEN = "\033[32m"; |
|||
private static final String ANSI_RED = "\033[31m"; |
|||
private static final String ANSI_CYAN = "\033[36m"; |
|||
private static final String ANSI_RESET = "\033[0m"; |
|||
|
|||
private Scanner scanner = new Scanner(System.in); |
|||
|
|||
public String readLine() { |
|||
System.out.print("crawler> "); |
|||
return scanner.nextLine().trim(); |
|||
} |
|||
|
|||
public void print(String msg) { |
|||
System.out.println(msg); |
|||
} |
|||
|
|||
public void printSuccess(String msg) { |
|||
print(ANSI_GREEN + msg + ANSI_RESET); |
|||
} |
|||
|
|||
public void printError(String msg) { |
|||
print(ANSI_RED + msg + ANSI_RESET); |
|||
} |
|||
|
|||
public void printInfo(String msg) { |
|||
print(ANSI_CYAN + msg + ANSI_RESET); |
|||
} |
|||
|
|||
// 展示文章列表 |
|||
public void display(List<Article> articles) { |
|||
if (articles.isEmpty()) { |
|||
printInfo("No articles yet. Use 'crawl <url>' first."); |
|||
return; |
|||
} |
|||
print("+----------+--------------------------------+"); |
|||
print("| Title | URL |"); |
|||
print("+----------+--------------------------------+"); |
|||
for (Article a : articles) { |
|||
String title = a.getTitle(); |
|||
if (title.length() > 10) title = title.substring(0, 10) + ".."; |
|||
String url = a.getUrl(); |
|||
if (url.length() > 30) url = url.substring(0, 27) + "..."; |
|||
print("| " + String.format("%-10s", title) + " | " + url + " |"); |
|||
} |
|||
print("+----------+--------------------------------+"); |
|||
printInfo("Total: " + articles.size() + " articles"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**教师提示**: |
|||
> "注意:所有ANSI转义码都被定义为`private static final`常量。如果把`\033[32m`散落在项目各处,一旦想调整颜色,就得满世界去改——这正是我们之前痛批的'意大利面'。**这就是工程细节**。" |
|||
|
|||
#### 4.6.3 Command接口与四个实现(全部通过View输出) |
|||
|
|||
```java |
|||
// Command.java |
|||
public interface Command { |
|||
String getName(); |
|||
void execute(String[] args, List<Article> articles); |
|||
} |
|||
|
|||
// HelpCommand.java |
|||
public class HelpCommand implements Command { |
|||
private ConsoleView view; |
|||
public HelpCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "help"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.printInfo("Commands: crawl <url>, list, help, exit"); |
|||
} |
|||
} |
|||
|
|||
// ListCommand.java |
|||
public class ListCommand implements Command { |
|||
private ConsoleView view; |
|||
public ListCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "list"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.display(articles); |
|||
} |
|||
} |
|||
|
|||
// CrawlCommand.java (存根) |
|||
public class CrawlCommand implements Command { |
|||
private ConsoleView view; |
|||
public CrawlCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "crawl"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
if (args.length < 2) { |
|||
view.printError("Usage: crawl <url>"); |
|||
return; |
|||
} |
|||
view.printInfo("Stub: Would crawl " + args[1]); |
|||
} |
|||
} |
|||
|
|||
// ExitCommand.java |
|||
public class ExitCommand implements Command { |
|||
private ConsoleView view; |
|||
public ExitCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "exit"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.printSuccess("Bye!"); // 全部输出都通过View,绝不让System.out直接出现在这里 |
|||
System.exit(0); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**故意埋设的"找茬点"**: |
|||
> "我在刚才的代码里有没有隐藏违反MVC原则的地方?`CrawlCommand`的存根里,`view.printInfo("Stub: Would crawl " + args[1]);` —— 这个字符串拼接算是"业务逻辑"吗?留给大家用AI架构审计时讨论。 |
|||
|
|||
#### 4.6.4 Controller:Map路由(全部通过View输出) |
|||
|
|||
```java |
|||
// src/main/java/com/crawler/controller/CrawlerController.java |
|||
package com.crawler.controller; |
|||
|
|||
import com.crawler.command.*; |
|||
import com.crawler.model.Article; |
|||
import com.crawler.view.ConsoleView; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class CrawlerController { |
|||
private Map<String, Command> commands = new HashMap<>(); |
|||
private ConsoleView view; // 持有View |
|||
private List<Article> articles; |
|||
|
|||
public CrawlerController(ConsoleView view, List<Article> articles) { |
|||
this.view = view; |
|||
this.articles = articles; |
|||
commands.put("help", new HelpCommand(view)); |
|||
commands.put("list", new ListCommand(view)); |
|||
commands.put("crawl", new CrawlCommand(view)); |
|||
commands.put("exit", new ExitCommand(view)); |
|||
} |
|||
|
|||
public void handle(String input) { |
|||
if (input.isEmpty()) return; |
|||
String[] parts = input.split("\\s+"); |
|||
String cmdName = parts[0].toLowerCase(); |
|||
|
|||
Command cmd = commands.get(cmdName); |
|||
if (cmd == null) { |
|||
view.printError("Unknown command: " + cmdName); // 错误信息也走View! |
|||
return; |
|||
} |
|||
cmd.execute(parts, articles); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### 4.6.5 main方法:组装 |
|||
|
|||
```java |
|||
// src/main/java/com/crawler/App.java |
|||
package com.crawler; |
|||
|
|||
import com.crawler.controller.CrawlerController; |
|||
import com.crawler.model.Article; |
|||
import com.crawler.view.ConsoleView; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class App { |
|||
public static void main(String[] args) { |
|||
ConsoleView view = new ConsoleView(); |
|||
List<Article> articles = new ArrayList<>(); |
|||
CrawlerController controller = new CrawlerController(view, articles); |
|||
|
|||
view.printSuccess("Welcome to CLI Crawler!"); |
|||
view.printInfo("Type 'help' for commands."); |
|||
|
|||
while (true) { |
|||
controller.handle(view.readLine()); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### 4.6.6 架构反思与展望:共享List<Article>的隐患(关键!) |
|||
|
|||
**教师口播**: |
|||
> "现在这个架构已经可用了。但请大家审视一下:我们所有的Command都直接拿到了`List<Article>`的引用。换句话说,任何一个命令都可以随意增、删、改这个列表。" |
|||
> |
|||
> "这就好像一家酒店,所有服务员、厨师、清洁工都能随意进出保险箱——**数据结构完全裸奔了**。" |
|||
|
|||
**提问**: |
|||
- "如果CrawlCommand不小心写错了代码,把一个null塞进articles,HelpCommand会不会受影响?" |
|||
- "如果未来我们要在添加文章时也写入日志文件,现在的设计能优雅实现吗?还是得在所有Command里分别加日志代码?" |
|||
|
|||
**预告解决方案**: |
|||
> "下周,我们将引入**策略模式**和一个真正的**Model仓库层(ArticleRepository)**。这个仓库会把`List`封装起来,对外只提供`add()`、`getAll()`等安全接口。任何命令想修改数据,都必须通过仓库。这就是从'数据结构'到'模型层'的进化——我们W9先搭骨架,W10给它装上盔甲。" |
|||
|
|||
--- |
|||
|
|||
### 4.7 实践任务(5分钟) |
|||
|
|||
**任务要求**: |
|||
1. 使用Maven模板创建项目 |
|||
2. 实现完整包结构(model/view/command/controller) |
|||
3. 实现4个Command:help/list/crawl/exit |
|||
4. `list`命令能展示已抓取的文章 |
|||
5. 运行并测试循环 |
|||
6. **代码找茬(额外加分)**:找出你自己代码中是否存在`System.out`直接调用、硬编码ANSI字符串等"越权行为" |
|||
|
|||
**验收标准**: |
|||
- [x] Maven编译通过 |
|||
- [x] Command接口和4个实现分离在不同文件 |
|||
- [x] Controller里没有switch-case |
|||
- [x] 新增命令只需新建类,不改Controller |
|||
- [x] list命令能正确显示空列表 |
|||
- [x] 所有输出均通过ConsoleView完成,无直接System.out.println(main除外) |
|||
- [x] ANSI颜色码集中定义为View常量 |
|||
|
|||
--- |
|||
|
|||
## 五、课后作业 |
|||
|
|||
### 5.1 必做任务 |
|||
|
|||
1. **完善Article**:增加`author`、`publishDate`字段 |
|||
2. **★ HistoryCommand(强制作业)**: |
|||
- 实现`history`命令,记录用户输入过的所有命令 |
|||
- 使用`List<String>`存储历史(复习W8集合) |
|||
- 示例输出: |
|||
``` |
|||
crawler> history |
|||
1. help |
|||
2. list |
|||
3. crawl https://example.com |
|||
``` |
|||
3. **AI架构审计**:将类名和方法名发给AI,指令: |
|||
> "作为Java架构审计师,请检查我的MVC三层划分是否存在越权行为?Model层是否包含输入输出代码?View层是否越权写了业务逻辑?有没有地方直接使用了System.out或硬编码ANSI码?" |
|||
|
|||
### 5.2 选做任务 |
|||
|
|||
1. **命令别名**:给`crawl`增加别名`c`,`help`增加别名`h` |
|||
2. **URL验证**:检查URL格式是否以http://或https://开头 |
|||
3. **暗色主题**:实现不同的配色方案(利用View中的ANSI常量,只需修改一处即可) |
|||
4. **思考并回答**:分析`List<Article>`共享引用的潜在风险,写一段200字的小结 |
|||
|
|||
### 5.3 思考题 |
|||
|
|||
1. **Command vs switch-case**:增加10个命令,哪种方式代码改动量更小? |
|||
2. **如果不用Command接口,直接用Map存命令类行不行?** 接口的意义是什么? |
|||
3. **Controller里的`commands.put()`能否减少?** 提示:思考"注册机制" |
|||
4. **为什么ExitCommand里的`view.printSuccess("Bye!")`比直接`System.out.println`更"MVC"?** 提示:回忆View的职责 |
|||
|
|||
--- |
|||
|
|||
## 六、AI协同升级 |
|||
|
|||
### 架构审计师任务(必做) |
|||
|
|||
**学生执行步骤**: |
|||
1. 列出项目中所有类名(不含方法实现) |
|||
2. 将类名列表发给AI |
|||
3. 输入指令: |
|||
> "作为Java架构审计师,请检查我的MVC三层划分是否清晰。Model层是否包含了不应该有的代码(Scanner/System.out)?View层是否越权写了业务逻辑?请指出任何一处直接使用System.out.println的地方,并建议如何改正。" |
|||
|
|||
**预期AI输出**: |
|||
- 指出哪一层有越权行为 |
|||
- 建议如何整改 |
|||
- 评价整体架构健康度 |
|||
|
|||
### 进阶AI探究(选做) |
|||
|
|||
> "假设我的Command接口中execute方法接收了一个`List<Article>`参数,请分析这种设计在工程上有什么隐患,并给出重构建议。" |
|||
|
|||
--- |
|||
|
|||
## 七、教学反思与调整记录 |
|||
|
|||
| 日期 | 事项 | 调整内容 | |
|||
|------|------|----------| |
|||
| 2026-04-28 | 首次编写 | 基于CLI+MVC重构 | |
|||
| 2026-04-30 | 教授反馈 | 引入Command模式、提供Maven模板、升级AI协同比 | |
|||
| 2026-04-30 | 逻辑重排 | 按"问题→选择→架构→模式"顺序重写 | |
|||
| 2026-05-01 | v2 vs V3合并 | 融合深度改进:增加教育哲学、批判性思考、ANSI常量、共享List隐患、故意埋坑 | |
|||
|
|||
--- |
|||
|
|||
## 附录1:Maven模板说明 |
|||
|
|||
> 老师提供`my-crawler-template.zip`压缩包,包含: |
|||
> - pom.xml(含Jsoup依赖) |
|||
> - 空的src/main/java结构 |
|||
> - .gitignore |
|||
|
|||
## 附录2:常见问题速查 |
|||
|
|||
| 问题 | 解答 | |
|||
|------|------| |
|||
| IDEA不识别pom.xml | 右键 pom.xml → Maven → Reload Project | |
|||
| 中文乱码 | Settings → Editor → File Encodings → UTF-8 | |
|||
| 包名大小写 | 包名全小写,类名首字母大写 | |
|||
| Command找不到 | 检查是否 implements Command,是否 @Override getName() | |
|||
| 命令不生效 | 检查 commands.put() 是否注册了该命令 | |
|||
| 输出颜色乱码 | IDEA控制台需支持ANSI,Windows下建议使用Windows Terminal或调整设置 | |
|||
| 我的System.out为什么被老师说越权 | View层才是与用户交互的唯一出口,所有输出都应通过View,这样将来改成GUI或日志时只需改View | |
|||
|
|||
## 附录3:教学逻辑说明 |
|||
|
|||
| 顺序 | 内容 | 设计理由 | |
|||
|------|------|----------| |
|||
| 1 | 痛点引入 | 从问题出发,让学生感受"为什么需要架构" | |
|||
| 2 | CLI vs GUI | 解释技术选型,建立"工程思维 > 视觉装饰"的认知 | |
|||
| 3 | MVC分层 | 核心架构概念,理解职责分离,通过类比及批判加深理解 | |
|||
| 4 | Command模式 | 具体实现方式,解决"肥控制器"问题 | |
|||
| 5 | Maven | 工具链支持 | |
|||
| 6 | 代码落地 | 实践验证,刻意植入细节规范,训练工程洁癖 | |
|||
| 7 | 架构反思 | 暴露共享可变状态隐患,为W10策略模式+仓库层做铺垫 | |
|||
| 8 | 实践任务 | 现场编码验证 | |
|||
| 9 | 总结 | 强化认知,预告下周 | |
|||
|
|||
--- |
|||
|
|||
## 版本说明 |
|||
|
|||
- **v1**:首次编写,CLI+MVC基础框架 |
|||
- **v2**:按"问题→选择→架构→模式"逻辑重排 |
|||
- **v3 (本版)**:融合v2结构 + V3深度改进,包含: |
|||
- 更深的CLI教育哲学 |
|||
- 餐厅类比批判性思考 |
|||
- ANSI常量集中管理工程细节 |
|||
- 全部输出走View |
|||
- 共享List架构隐患反思 |
|||
- 故意埋坑让学生找茬 |
|||
- W10铺垫(策略模式+仓库层) |
|||
@ -1,5 +0,0 @@ |
|||
#Generated by Maven |
|||
#Thu Apr 30 11:50:54 CST 2026 |
|||
artifactId=datacollect-cli |
|||
groupId=com.example |
|||
version=0.1.0 |
|||
@ -1,530 +0,0 @@ |
|||
## 高级程序设计 · 第9周 |
|||
|
|||
#### 工程架构:从"写代码"到"造系统" |
|||
|
|||
##### CLI + MVC + Command模式实战 |
|||
|
|||
--- |
|||
|
|||
### 📌 本周导航 |
|||
|
|||
- 痛点引入:脚本的宿命 |
|||
- CLI vs GUI:为什么选命令行? |
|||
- MVC分层:职责分离的艺术 |
|||
- Command模式:可扩展的路由 |
|||
- Maven模板:工程化第一步 |
|||
- 代码落地:从接口到实现 |
|||
- 架构反思:共享数据的隐患 |
|||
- 实践任务 + 课后作业 |
|||
|
|||
--- |
|||
|
|||
### 1️⃣ 痛点引入:从脚本到工程的鸿沟 |
|||
|
|||
#### 这是一段“意大利面”爬虫 |
|||
|
|||
```java |
|||
public class Crawler { |
|||
public static void main(String[] args) { |
|||
System.out.print("请输入URL: "); |
|||
Scanner scanner = new Scanner(System.in); |
|||
String url = scanner.nextLine(); |
|||
List titles = new ArrayList(); |
|||
try { |
|||
Document doc = Jsoup.connect(url).get(); |
|||
Elements elements = doc.select(".post-title"); |
|||
for (Element e : elements) { |
|||
String title = e.text(); |
|||
System.out.println("标题: " + title); |
|||
titles.add(title); |
|||
} |
|||
} catch (Exception ex) { |
|||
System.out.println("出错啦: " + ex.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
### 脚本的三大痛点 |
|||
|
|||
| 需求 | 需要改哪里? | |
|||
|------|--------------| |
|||
| 保存标题到文件 | 改 main 内部逻辑 | |
|||
| 支持不同网站结构 | 全部重写解析代码 | |
|||
| 彩色输出 | 一个一个改 print | |
|||
|
|||
> 😫 **牵一发而动全身 → 改起来疼** |
|||
|
|||
### 本周目标:**让代码“改起来不疼”** |
|||
|
|||
--- |
|||
|
|||
## 2️⃣ CLI vs GUI:架构选择的思考 |
|||
|
|||
### 图形界面 vs 命令行 |
|||
|
|||
| 维度 | GUI (JavaFX) | CLI (命令行) | |
|||
|------|--------------|-------------| |
|||
| 学习重心 | 布局、控件、事件 | **架构、分层、路由** | |
|||
| 后端能力 | 弱 | 模拟真实服务器 | |
|||
| 工程思维 | 弱(关注视觉) | **强(关注逻辑)** | |
|||
| 可测试性 | 难 | 易 | |
|||
|
|||
--- |
|||
|
|||
## 核心观点 |
|||
|
|||
> **CLI 更需要 MVC!** |
|||
|
|||
- GUI 有现成事件系统,框架强塞给你一套架构 |
|||
- CLI 只有字符流 → **没有架构,分分钟写成脚本** |
|||
|
|||
> 🎯 **当外部约束消失,内部的工程纪律才真正开始建立** |
|||
|
|||
### CLI 也能很酷 |
|||
|
|||
- ANSI 彩色输出 |
|||
- 表格展示数据 |
|||
- 模拟大数据/后端开发 |
|||
|
|||
--- |
|||
|
|||
## 3️⃣ MVC 分层设计 |
|||
|
|||
### MVC 的起源与演进 |
|||
|
|||
| 年代 | 场景 | MVC的角色 | |
|||
|------|------|----------| |
|||
| 1970s | Smalltalk-72 GUI | 最早的用户界面架构 | |
|||
| 1990s | Web开发 (Struts) | 后端模板引擎 | |
|||
| 2000s | ASP.NET MVC | 现代Web框架 | |
|||
| 2020s | CLI + API | 解耦业务逻辑与表现层 | |
|||
|
|||
**核心不变:职责分离** |
|||
|
|||
--- |
|||
|
|||
## MVC 三层职责 |
|||
|
|||
![[mvc.png]] |
|||
``` |
|||
┌─────────────────────────────────────────┐ |
|||
│ 入口 │ |
|||
│ (main方法) │ |
|||
└─────────────────┬───────────────────────┘ |
|||
▼ |
|||
┌─────────────────────────────────────────┐ |
|||
│ Controller │ |
|||
│ 只管"派给谁",不管"怎么做" │ |
|||
└─────────┬───────────────┬───────────────┘ |
|||
▼ ▼ |
|||
┌─────────────────┐ ┌─────────────────┐ |
|||
│ Model │ │ View │ |
|||
│ 管"数据" │ │ 管"呈现" │ |
|||
│ + 业务逻辑 │ │ + 输入输出 │ |
|||
└─────────────────┘ └─────────────────┘ |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 三层“禁止做什么” |
|||
|
|||
| 层级 | 禁止行为 | |
|||
| -------------- | -------------------------------------- | |
|||
| **Model** | 不能有 `System.out.println`,不能有 `Scanner` | |
|||
| **View** | 不能写爬虫逻辑,只做“传声筒” | |
|||
| **Controller** | 不能直接写业务细节,委托给 Command | |
|||
|
|||
> 🔴 **越权就是架构腐败的开始** |
|||
|
|||
--- |
|||
|
|||
## 🍽️ 餐厅类比(帮助理解) |
|||
|
|||
- **Model = 后厨**:只管做菜,不管谁来吃、怎么端 |
|||
- **View = 服务员**:只管端菜和收钱,不管菜怎么做 |
|||
- **Controller = 前台**:接单 → 派给后厨 → 叫服务员上菜 |
|||
|
|||
--- |
|||
|
|||
## 🤔 对类比的批判性思考(关键!) |
|||
|
|||
> 任何类比都有边界,不要当成真理 |
|||
|
|||
| 场景 | 暴露的问题 | |
|||
|------|------------| |
|||
| 客人有忌口(不吃香菜) | 信息需要传到后厨 → Model 可能需要知道 meta 信息 | |
|||
| 服务员反馈“今天的菜咸了” | View → Model 反向影响 | |
|||
| 后厨做完菜通知前台 | **观察者模式**,数据流可能是双向的 | |
|||
|
|||
**本课程简化模型**:请求-响应,单向流 |
|||
|
|||
--- |
|||
|
|||
## MVC 数据流向(本课程简化版) |
|||
|
|||
``` |
|||
CLI用户输入 |
|||
↓ |
|||
View(解析命令字符串) |
|||
↓ |
|||
Controller(找到对应Command) |
|||
↓ |
|||
Command.execute()(执行业务逻辑) |
|||
↓ |
|||
Model(Article数据,暂存于List) |
|||
↓ |
|||
View(display()展示数据) |
|||
↓ |
|||
CLI终端显示 |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 4️⃣ Command 模式:可扩展的命令路由 |
|||
|
|||
### 为什么需要 Command 模式? |
|||
|
|||
```java |
|||
switch (cmd) { |
|||
case "crawl": handleCrawl(); break; |
|||
case "help": showHelp(); break; |
|||
// 如果要增加 list 命令? |
|||
// 1. 加 case "list" |
|||
// 2. 加 handleList() 方法 |
|||
// 3. 可能还要改其他地方... |
|||
} |
|||
``` |
|||
|
|||
> 每加一个功能,就要在这个类里戳一个洞 → **肥控制器陷阱** |
|||
|
|||
--- |
|||
|
|||
## Command 模式的四个要素 |
|||
|
|||
| 要素 | 角色 | 示例 | |
|||
|------|------|------| |
|||
| Command接口 | 抽象的“订单” | `Command` | |
|||
| ConcreteCommand | 具体的订单 | `HelpCommand` | |
|||
| Invoker | 接单的前台 | `CrawlerController` | |
|||
| Receiver | 执行者 | `ConsoleView`、`ArticleRepository` | |
|||
|
|||
--- |
|||
|
|||
## Command 接口定义 |
|||
|
|||
```java |
|||
package com.crawler.command; |
|||
|
|||
import com.crawler.model.Article; |
|||
import java.util.List; |
|||
|
|||
public interface Command { |
|||
String getName(); |
|||
void execute(String[] args, List<Article> articles); |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Controller 的变革:从 switch 到 Map |
|||
|
|||
```java |
|||
public class CrawlerController { |
|||
private Map<String, Command> commands = new HashMap<>(); |
|||
|
|||
public CrawlerController(ConsoleView view, List<Article> articles) { |
|||
commands.put("help", new HelpCommand(view)); |
|||
commands.put("list", new ListCommand(view)); |
|||
commands.put("crawl", new CrawlCommand(view)); |
|||
commands.put("exit", new ExitCommand(view)); |
|||
} |
|||
|
|||
public void handle(String input) { |
|||
// 解析命令 → 从 Map 取 Command → 调用 execute |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> **增加新命令:只需新建类,Controller 零改动!** |
|||
|
|||
--- |
|||
|
|||
## 对比:switch-case vs Command |
|||
|
|||
| 维度 | switch-case | Command模式 | |
|||
|------|-------------|-------------| |
|||
| 增加命令 | 要改 Controller | 新建一个类 | |
|||
| 多态体验 | 无 | `execute()` 多态 | |
|||
| 可测试性 | 难 | 每个 Command 单独测试 | |
|||
| 代码量 | 少 | 多,但更清晰 | |
|||
|
|||
> 🏨 **类比:酒店客房服务,前台只负责派单** |
|||
|
|||
--- |
|||
|
|||
## 5️⃣ Maven 模板与环境(5分钟) |
|||
|
|||
### 直接使用模板,不折腾配置 |
|||
|
|||
``` |
|||
my-crawler-template.zip |
|||
↓ 解压 + IDEA打开 |
|||
↓ 右键 pom.xml → Maven → Reload Project |
|||
↓ 运行 App.java |
|||
``` |
|||
|
|||
### 标准目录结构 |
|||
|
|||
``` |
|||
src/main/java/com/crawler/ |
|||
├── model/Article.java |
|||
├── view/ConsoleView.java |
|||
├── command/ |
|||
│ ├── Command.java |
|||
│ ├── CrawlCommand.java |
|||
│ ├── HelpCommand.java |
|||
│ ├── ListCommand.java |
|||
│ └── ExitCommand.java |
|||
└── controller/CrawlerController.java |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## 6️⃣ 代码落地(分步实现) |
|||
|
|||
### Model:Article 实体 |
|||
|
|||
```java |
|||
public class Article { |
|||
private String title; |
|||
private String url; |
|||
private String content; |
|||
// 构造器、getter/setter、toString |
|||
} |
|||
``` |
|||
|
|||
> 📦 只存放数据,没有任何输入输出代码 |
|||
|
|||
--- |
|||
|
|||
## View:ConsoleView(ANSI常量集中管理) |
|||
|
|||
```java |
|||
public class ConsoleView { |
|||
private static final String ANSI_GREEN = "\033[32m"; |
|||
private static final String ANSI_RED = "\033[31m"; |
|||
// ... 其他常量 |
|||
|
|||
public void printSuccess(String msg) { |
|||
System.out.println(ANSI_GREEN + msg + ANSI_RESET); |
|||
} |
|||
public void printError(String msg) { ... } |
|||
public void display(List<Article> articles) { ... } |
|||
} |
|||
``` |
|||
|
|||
> ✨ **所有颜色码集中定义 → 改主题只需改一处** |
|||
|
|||
--- |
|||
|
|||
## Command 实现示例(HelpCommand) |
|||
|
|||
```java |
|||
public class HelpCommand implements Command { |
|||
private ConsoleView view; |
|||
public HelpCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "help"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.printInfo("Commands: crawl <url>, list, help, exit"); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> ⚠️ 全部输出通过 `view`,绝不让 `System.out` 直接出现在这里 |
|||
|
|||
--- |
|||
|
|||
## CrawlCommand(存根,下周填坑) |
|||
|
|||
```java |
|||
public class CrawlCommand implements Command { |
|||
private ConsoleView view; |
|||
public CrawlCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "crawl"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
if (args.length < 2) { |
|||
view.printError("Usage: crawl <url>"); |
|||
return; |
|||
} |
|||
view.printInfo("Stub: Would crawl " + args[1]); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> 🔍 **找茬点**:这里拼接字符串算是“业务逻辑”吗?留给大家用 AI 审计。 |
|||
|
|||
--- |
|||
|
|||
## ExitCommand |
|||
|
|||
```java |
|||
public class ExitCommand implements Command { |
|||
private ConsoleView view; |
|||
public ExitCommand(ConsoleView v) { this.view = v; } |
|||
public String getName() { return "exit"; } |
|||
public void execute(String[] args, List<Article> articles) { |
|||
view.printSuccess("Bye!"); |
|||
System.exit(0); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> ✅ 所有输出都通过 View → 将来改 GUI 只需换 View 实现 |
|||
|
|||
--- |
|||
|
|||
## Controller + main 组装 |
|||
|
|||
```java |
|||
// Controller 中持有 Map<String,Command> |
|||
// App.java 中: |
|||
ConsoleView view = new ConsoleView(); |
|||
List<Article> articles = new ArrayList<>(); |
|||
CrawlerController controller = new CrawlerController(view, articles); |
|||
view.printSuccess("Welcome to CLI Crawler!"); |
|||
while (true) { |
|||
controller.handle(view.readLine()); |
|||
} |
|||
``` |
|||
|
|||
> 🔁 完成交互循环 |
|||
|
|||
--- |
|||
|
|||
## 7️⃣ 架构反思:共享 List<Article> 的隐患 |
|||
|
|||
### 当前问题 |
|||
|
|||
- 所有 Command 都直接拿到 `List<Article>` 引用 |
|||
- 任何一个命令都可以随意增、删、改列表 |
|||
- 数据完全“裸奔” |
|||
|
|||
> 🚨 就像酒店所有员工都能进保险箱 |
|||
|
|||
--- |
|||
|
|||
## 提问 |
|||
|
|||
- 如果 `CrawlCommand` 不小心把 `null` 塞进列表,`ListCommand` 会怎样? |
|||
- 如果我们要在添加文章时写日志,现在的设计能优雅实现吗? |
|||
|
|||
### 预告解决方案(W10) |
|||
|
|||
- **策略模式** + **仓库层(ArticleRepository)** |
|||
- 封装 `List`,对外只暴露 `add()`、`getAll()` 等安全接口 |
|||
|
|||
> W9 搭骨架,W10 装上盔甲 |
|||
|
|||
--- |
|||
|
|||
## 8️⃣ 实践任务(现场5分钟) |
|||
|
|||
### 必做项 |
|||
|
|||
1. 使用 Maven 模板创建项目 |
|||
2. 实现完整包结构(model/view/command/controller) |
|||
3. 实现 4 个 Command:help / list / crawl / exit |
|||
4. `list` 能展示已抓取的文章(目前存根即可) |
|||
5. 运行并测试循环 |
|||
|
|||
### 额外加分:代码找茬 |
|||
|
|||
- 检查是否仍有 `System.out` 直接调用 |
|||
- 检查 ANSI 码是否硬编码在多个地方 |
|||
|
|||
--- |
|||
|
|||
## 验收标准 |
|||
|
|||
- [x] Maven 编译通过 |
|||
- [x] Command 接口和 4 个实现在不同文件 |
|||
- [x] Controller 里没有 switch-case |
|||
- [x] 新增命令只需新建类,不改 Controller |
|||
- [x] list 能正确显示空列表 |
|||
- [x] 所有输出均通过 `ConsoleView` |
|||
- [x] ANSI 颜色码集中定义为常量 |
|||
|
|||
--- |
|||
|
|||
## 9️⃣ 课后作业 |
|||
|
|||
### 必做 |
|||
|
|||
1. **完善 Article**:增加 `author`、`publishDate` 字段 |
|||
2. **★ HistoryCommand**:记录用户输入过的所有命令(用 `List<String>`) |
|||
3. **AI 架构审计**:将类名发给 AI,指令: |
|||
> “作为Java架构审计师,请检查我的MVC三层划分是否存在越权行为?” |
|||
|
|||
### 选做 |
|||
|
|||
- 命令别名(c 代替 crawl) |
|||
- URL 格式验证 |
|||
- 暗色主题(修改一处常量) |
|||
- 思考题:分析 `List<Article>` 共享引用的风险(200字小结) |
|||
|
|||
--- |
|||
|
|||
## 🤖 AI 协同升级 |
|||
|
|||
### 架构审计师任务(必做) |
|||
|
|||
**步骤**: |
|||
1. 列出所有类名(不含方法实现) |
|||
2. 发给 AI |
|||
3. 指令:“检查 MVC 分层是否清晰,是否有越权行为” |
|||
|
|||
### 进阶探究(选做) |
|||
|
|||
> “假设我的 Command 接口中 execute 方法接收了一个 `List<Article>` 参数,请分析这种设计在工程上有什么隐患,并给出重构建议。” |
|||
|
|||
--- |
|||
|
|||
## 📚 总结与过渡 |
|||
|
|||
### 本周成果 |
|||
|
|||
- ✅ 工程化包结构 |
|||
- ✅ MVC 分层清晰 |
|||
- ✅ Command 模式实现可扩展路由 |
|||
- ✅ 所有输出走 View,常量集中管理 |
|||
|
|||
### 下周预告 |
|||
|
|||
- **策略模式**:封装爬取算法 |
|||
- **仓库层(Repository)**:武装 `List<Article>`,解决共享隐患 |
|||
|
|||
> 🚀 从“写代码”到“造系统”,踏出坚实第一步! |
|||
|
|||
--- |
|||
|
|||
## Q&A |
|||
|
|||
### 常见问题 |
|||
|
|||
| 问题 | 解答 | |
|||
|------|------| |
|||
| IDEA 不识别 pom.xml | 右键 → Maven → Reload Project | |
|||
| 中文乱码 | Settings → File Encodings → UTF-8 | |
|||
| 输出颜色乱码 | Windows 建议使用 Windows Terminal | |
|||
| 我的 System.out 被批评 | View 才是唯一输出出口 | |
|||
|
|||
--- |
|||
|
|||
## 谢谢! |
|||
|
|||
### 课件已上传,模板在课程群 |
|||
|
|||
**保持工程洁癖,下周见!** |
|||
@ -1,13 +0,0 @@ |
|||
# TemperatureConverter 温度转换程序 |
|||
## 项目简介 |
|||
这是一个用 Java 实现的温度转换工具,支持**摄氏温度(℃)**与**华氏温度(℉)**的双向转换,同时提供两种运行模式: |
|||
1. 交互式控制台模式(手动选择功能并输入温度) |
|||
2. 命令行参数模式(一键直接转换,无需交互) |
|||
|
|||
核心转换公式: |
|||
- 摄氏温度 → 华氏温度:`F = C × 1.8 + 32` |
|||
- 华氏温度 → 摄氏温度:`C = (F - 32) ÷ 1.8` |
|||
|
|||
--- |
|||
|
|||
## 文件结构 |
|||
@ -0,0 +1,108 @@ |
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 import java.io.BufferedReader;\ |
|||
import java.io.FileReader;\ |
|||
import java.io.File;\ |
|||
import java.util.Scanner;\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u28201 \u24230 \u36716 \u25442 \u22120 \u31243 \u24207 \u65288 Java\u65289 \ |
|||
* \uc0\u31561 \u25928 \u31227 \u26893 Python\u28201 \u24230 \u36716 \u25442 \u31243 \u24207 \u65292 \u25903 \u25345 \u25668 \u27663 /\u21326 \u27663 \u20114 \u36716 \ |
|||
* \uc0\u39069 \u22806 \u25903 \u25345 \u65306 \u21629 \u20196 \u34892 \u21442 \u25968 \u27169 \u24335 \u12289 \u25991 \u20214 \u25209 \u37327 \u36716 \u25442 \u27169 \u24335 \ |
|||
*/\ |
|||
public class TemperatureConverter \{\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u25668 \u27663 \u24230 \u36716 \u25442 \u20026 \u21326 \u27663 \u24230 \ |
|||
* @param c \uc0\u36755 \u20837 \u30340 \u25668 \u27663 \u28201 \u24230 \u65292 \u25968 \u25454 \u31867 \u22411 \u20026 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
* @return \uc0\u36716 \u25442 \u21518 \u30340 \u21326 \u27663 \u28201 \u24230 \u65292 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
*/\ |
|||
public static double celsiusToFahrenheit(double c) \{\ |
|||
return c * 9.0 / 5.0 + 32.0;\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u21326 \u27663 \u24230 \u36716 \u25442 \u20026 \u25668 \u27663 \u24230 \ |
|||
* @param f \uc0\u36755 \u20837 \u30340 \u21326 \u27663 \u28201 \u24230 \u65292 \u25968 \u25454 \u31867 \u22411 \u20026 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
* @return \uc0\u36716 \u25442 \u21518 \u30340 \u25668 \u27663 \u28201 \u24230 \u65292 \u21452 \u31934 \u24230 \u28014 \u28857 \u22411 \ |
|||
*/\ |
|||
public static double fahrenheitToCelsius(double f) \{\ |
|||
return (f - 32.0) * 5.0 / 9.0;\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u25209 \u37327 \u36716 \u25442 \u65306 \u20174 \u25991 \u20214 \u35835 \u21462 \u22810 \u34892 \u28201 \u24230 \u25968 \u25454 \u24182 \u23436 \u25104 \u36716 \u25442 \u65288 \u21152 \u20998 \u39033 \u65289 \ |
|||
* @param filename \uc0\u23384 \u20648 \u28201 \u24230 \u25968 \u25454 \u30340 \u25991 \u20214 \u21517 \u65292 \u23383 \u31526 \u20018 \u31867 \u22411 \ |
|||
*/\ |
|||
public static void batchConvert(String filename) \{\ |
|||
try (BufferedReader br = new BufferedReader(new FileReader(filename))) \{\ |
|||
String line;\ |
|||
while ((line = br.readLine()) != null) \{\ |
|||
convertAndPrint(line.trim()); // \uc0\u35843 \u29992 \u32479 \u19968 \u36716 \u25442 \u26041 \u27861 \ |
|||
\}\ |
|||
\} catch (Exception e) \{\ |
|||
System.out.println("\uc0\u25991 \u20214 \u35835 \u21462 \u22833 \u36133 \u65306 " + e.getMessage());\ |
|||
\}\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u32479 \u19968 \u36716 \u25442 \u36923 \u36753 \u65306 \u35299 \u26512 \u36755 \u20837 \u24182 \u36755 \u20986 \u32467 \u26524 \u65288 \u22797 \u29992 \u26680 \u24515 \u36923 \u36753 \u65289 \ |
|||
* @param input \uc0\u24453 \u35299 \u26512 \u30340 \u28201 \u24230 \u19982 \u21333 \u20301 \u23383 \u31526 \u20018 \u65292 \u22914 "36.6 C"\ |
|||
*/\ |
|||
public static void convertAndPrint(String input) \{\ |
|||
if (input.isEmpty()) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u20026 \u31354 \u65292 \u36339 \u36807 \u36716 \u25442 \u12290 ");\ |
|||
return;\ |
|||
\}\ |
|||
String[] parts = input.split("\\\\s+");\ |
|||
try \{\ |
|||
double value = Double.parseDouble(parts[0]);\ |
|||
// \uc0\u19982 \u21407 Python\u36923 \u36753 \u19968 \u33268 \u65306 \u26410 \u36755 \u20837 \u21333 \u20301 \u26102 \u40664 \u35748 \u25353 \u25668 \u27663 \u24230 \u22788 \u29702 \ |
|||
String unit = parts.length > 1 ? parts[1].toUpperCase() : "C";\ |
|||
\ |
|||
if (unit.startsWith("C")) \{\ |
|||
double f = celsiusToFahrenheit(value);\ |
|||
System.out.printf("%.2f \'b0C = %.2f \'b0F%n", value, f);\ |
|||
\} else if (unit.startsWith("F")) \{\ |
|||
double c = fahrenheitToCelsius(value);\ |
|||
System.out.printf("%.2f \'b0F = %.2f \'b0C%n", value, c);\ |
|||
\} else \{\ |
|||
System.out.println("\uc0\u26410 \u30693 \u21333 \u20301 \u65292 \u35831 \u20351 \u29992 C \u25110 F\u12290 ");\ |
|||
\}\ |
|||
\} catch (Exception e) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u35299 \u26512 \u22833 \u36133 \u65292 \u35831 \u25353 \u31034 \u20363 \u36755 \u20837 \u65288 \u22914 36.6 C\u65289 \u12290 ");\ |
|||
\}\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u31243 \u24207 \u20027 \u20837 \u21475 \u65306 \u25972 \u21512 \u19977 \u31181 \u36816 \u34892 \u27169 \u24335 \ |
|||
* @param args \uc0\u21629 \u20196 \u34892 \u21442 \u25968 \u65292 \u25903 \u25345 \u26080 \u21442 \u25968 \u65288 \u20132 \u20114 \u24335 \u65289 \u12289 \u21442 \u25968 \u20026 \u28201 \u24230 \u21333 \u20301 \u65288 \u21629 \u20196 \u34892 \u27169 \u24335 \u65289 \u12289 \u21442 \u25968 \u20026 \u25991 \u20214 \u21517 \u65288 \u25209 \u37327 \u27169 \u24335 \u65289 \ |
|||
*/\ |
|||
public static void main(String[] args) \{\ |
|||
// \uc0\u27169 \u24335 1\u65306 \u25209 \u37327 \u25991 \u20214 \u36716 \u25442 \u65288 \u21152 \u20998 \u39033 \u65289 - \u20165 1\u20010 \u21442 \u25968 \u19988 \u20026 \u25991 \u20214 \u26102 \u35302 \u21457 \ |
|||
if (args.length == 1 && new File(args[0]).exists()) \{\ |
|||
batchConvert(args[0]);\ |
|||
return;\ |
|||
\}\ |
|||
\ |
|||
Scanner scanner = new Scanner(System.in);\ |
|||
String input;\ |
|||
\ |
|||
// \uc0\u27169 \u24335 2\u65306 \u21629 \u20196 \u34892 \u21442 \u25968 \u27169 \u24335 \u65288 \u21152 \u20998 \u39033 \u65289 - \u22810 \u21442 \u25968 \u26102 \u30452 \u25509 \u25340 \u25509 \u20026 \u36755 \u20837 \ |
|||
if (args.length > 0) \{\ |
|||
input = String.join(" ", args);\ |
|||
\} else \{\ |
|||
// \uc0\u27169 \u24335 3\u65306 \u20132 \u20114 \u24335 \u36755 \u20837 \u65288 \u21407 Python\u26680 \u24515 \u21151 \u33021 \u65292 \u24517 \u20570 \u65289 \ |
|||
System.out.print("\uc0\u35831 \u36755 \u20837 \u35201 \u36716 \u25442 \u30340 \u28201 \u24230 \u19982 \u21333 \u20301 \u65288 \u20363 \u22914 36.6 C \u25110 97 F\u65289 \u65306 ");\ |
|||
input = scanner.nextLine().trim();\ |
|||
\}\ |
|||
\ |
|||
convertAndPrint(input); // \uc0\u25191 \u34892 \u26680 \u24515 \u36716 \u25442 \ |
|||
scanner.close();\ |
|||
\}\ |
|||
\}} |
|||
@ -1,79 +1,74 @@ |
|||
import java.util.Scanner; |
|||
|
|||
|
|||
/** |
|||
* 温度转换程序:实现摄氏温度与华氏温度的互相转换 |
|||
* 支持控制台输入交互、命令行参数运行模式 |
|||
*/ |
|||
public class TemperatureConverter { |
|||
|
|||
/** |
|||
* 摄氏温度 转 华氏温度 |
|||
* @param celsius 输入的摄氏温度值 |
|||
* @return 转换后的华氏温度值,公式:F = C × 1.8 + 32 |
|||
*/ |
|||
public static double celsiusToFahrenheit(double celsius) { |
|||
return celsius * 1.8 + 32; |
|||
} |
|||
|
|||
/** |
|||
* 华氏温度 转 摄氏温度 |
|||
* @param fahrenheit 输入的华氏温度值 |
|||
* @return 转换后的摄氏温度值,公式:C = (F - 32) / 1.8 |
|||
*/ |
|||
public static double fahrenheitToCelsius(double fahrenheit) { |
|||
return (fahrenheit - 32) / 1.8; |
|||
} |
|||
|
|||
/** |
|||
* 主方法:程序入口,支持两种运行模式 |
|||
* 1. 无参数:控制台交互式输入 |
|||
* 2. 带参数:命令行参数模式 java TemperatureConverter 数值 温度类型(C/F) |
|||
* @param args 命令行参数 |
|||
*/ |
|||
public static void main(String[] args) { |
|||
// 命令行参数模式
|
|||
if (args.length == 2) { |
|||
try { |
|||
double temp = Double.parseDouble(args[0]); |
|||
String type = args[1].toUpperCase(); |
|||
if (type.equals("C")) { |
|||
double res = celsiusToFahrenheit(temp); |
|||
System.out.printf("%.2f ℃ = %.2f ℉%n", temp, res); |
|||
} else if (type.equals("F")) { |
|||
double res = fahrenheitToCelsius(temp); |
|||
System.out.printf("%.2f ℉ = %.2f ℃%n", temp, res); |
|||
} else { |
|||
System.out.println("温度类型仅支持 C(摄氏) / F(华氏)"); |
|||
} |
|||
} catch (Exception e) { |
|||
System.out.println("参数格式错误,示例:java TemperatureConverter 36.6 C"); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// 控制台交互模式
|
|||
Scanner scanner = new Scanner(System.in); |
|||
System.out.println("===== 温度转换器 ====="); |
|||
System.out.println("1. 摄氏温度转华氏温度"); |
|||
System.out.println("2. 华氏温度转摄氏温度"); |
|||
System.out.print("请选择功能(1/2):"); |
|||
int choice = scanner.nextInt(); |
|||
|
|||
System.out.print("请输入温度数值:"); |
|||
double value = scanner.nextDouble(); |
|||
|
|||
if (choice == 1) { |
|||
double result = celsiusToFahrenheit(value); |
|||
System.out.printf("转换结果:%.2f ℃ = %.2f ℉%n", value, result); |
|||
} else if (choice == 2) { |
|||
double result = fahrenheitToCelsius(value); |
|||
System.out.printf("转换结果:%.2f ℉ = %.2f ℃%n", value, result); |
|||
} else { |
|||
System.out.println("输入选项错误!"); |
|||
} |
|||
scanner.close(); |
|||
} |
|||
} |
|||
|
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 import java.util.Scanner;\ |
|||
\ |
|||
/**\ |
|||
* TemperatureConverter\ |
|||
* \uc0\u25903 \u25345 \u25668 \u27663 \u24230 (C)\u19982 \u21326 \u27663 \u24230 (F)\u20043 \u38388 \u20114 \u36716 \ |
|||
*/\ |
|||
public class TemperatureConverter \{\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u25668 \u27663 \u24230 \u36716 \u25442 \u20026 \u21326 \u27663 \u24230 \ |
|||
* @param c \uc0\u25668 \u27663 \u28201 \u24230 \ |
|||
* @return \uc0\u23545 \u24212 \u30340 \u21326 \u27663 \u28201 \u24230 \ |
|||
*/\ |
|||
public static double celsiusToFahrenheit(double c) \{\ |
|||
return c * 9.0 / 5.0 + 32.0;\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u21326 \u27663 \u24230 \u36716 \u25442 \u20026 \u25668 \u27663 \u24230 \ |
|||
* @param f \uc0\u21326 \u27663 \u28201 \u24230 \ |
|||
* @return \uc0\u23545 \u24212 \u30340 \u25668 \u27663 \u28201 \u24230 \ |
|||
*/\ |
|||
public static double fahrenheitToCelsius(double f) \{\ |
|||
return (f - 32.0) * 5.0 / 9.0;\ |
|||
\}\ |
|||
\ |
|||
public static void main(String[] args) \{\ |
|||
Scanner scanner = new Scanner(System.in);\ |
|||
\ |
|||
// \uc0\u25552 \u31034 \u29992 \u25143 \u36755 \u20837 \u65292 \u26684 \u24335 \u31034 \u20363 \u65306 "36.6 C" \u25110 "97 F"\
|
|||
System.out.print("\uc0\u35831 \u36755 \u20837 \u35201 \u36716 \u25442 \u30340 \u28201 \u24230 \u19982 \u21333 \u20301 \u65288 \u20363 \u22914 36.6 C \u25110 97 F\u65289 \u65306 ");\ |
|||
String input = scanner.nextLine().trim();\ |
|||
\ |
|||
if (input.isEmpty()) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u20026 \u31354 \u65292 \u31243 \u24207 \u36864 \u20986 \u12290 ");\ |
|||
scanner.close();\ |
|||
return;\ |
|||
\}\ |
|||
\ |
|||
String[] parts = input.split("\\\\s+");\ |
|||
\ |
|||
try \{\ |
|||
// \uc0\u35299 \u26512 \u25968 \u20540 \u21644 \u21333 \u20301 \
|
|||
double value = Double.parseDouble(parts[0]);\ |
|||
String unit = parts.length > 1 ? parts[1].toUpperCase() : "C";\ |
|||
\ |
|||
if (unit.startsWith("C")) \{\ |
|||
// \uc0\u20174 \u25668 \u27663 \u24230 \u36716 \u25442 \u20026 \u21326 \u27663 \u24230 \
|
|||
double f = celsiusToFahrenheit(value);\ |
|||
System.out.printf("%.2f \'b0C = %.2f \'b0F%n", value, f);\ |
|||
\} else if (unit.startsWith("F")) \{\ |
|||
// \uc0\u20174 \u21326 \u27663 \u24230 \u36716 \u25442 \u20026 \u25668 \u27663 \u24230 \
|
|||
double c = fahrenheitToCelsius(value);\ |
|||
System.out.printf("%.2f \'b0F = %.2f \'b0C%n", value, c);\ |
|||
\} else \{\ |
|||
System.out.println("\uc0\u26410 \u30693 \u21333 \u20301 \u65292 \u35831 \u20351 \u29992 C \u25110 F\u12290 ");\ |
|||
\}\ |
|||
\ |
|||
\} catch (NumberFormatException e) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u35299 \u26512 \u22833 \u36133 \u65292 \u35831 \u25353 \u31034 \u20363 \u36755 \u20837 \u25968 \u20540 \u19982 \u21333 \u20301 \u65292 \u20363 \u22914 \u65306 36.6 C");\ |
|||
\} catch (Exception e) \{\ |
|||
System.out.println("\uc0\u21457 \u29983 \u38169 \u35823 \u65306 " + e.getMessage());\ |
|||
\} finally \{\ |
|||
scanner.close();\ |
|||
\}\ |
|||
\}\ |
|||
\}} |
|||
@ -0,0 +1,72 @@ |
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 import java.util.Scanner;\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u25903 \u25345 \u25668 \u27663 \u24230 (C)\u19982 \u21326 \u27663 \u24230 (F)\u20043 \u38388 \u20114 \u36716 \ |
|||
*/\ |
|||
public class TemperatureConverter \{\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u25668 \u27663 \u24230 \u36716 \u25442 \u20026 \u21326 \u27663 \u24230 \ |
|||
* @param c \uc0\u25668 \u27663 \u28201 \u24230 \ |
|||
* @return \uc0\u23545 \u24212 \u30340 \u21326 \u27663 \u28201 \u24230 \ |
|||
*/\ |
|||
public static double celsiusToFahrenheit(double c) \{\ |
|||
return c * 9.0 / 5.0 + 32.0;\ |
|||
\}\ |
|||
\ |
|||
/**\ |
|||
* \uc0\u23558 \u21326 \u27663 \u24230 \u36716 \u25442 \u20026 \u25668 \u27663 \u24230 \ |
|||
* @param f \uc0\u21326 \u27663 \u28201 \u24230 \ |
|||
* @return \uc0\u23545 \u24212 \u30340 \u25668 \u27663 \u28201 \u24230 \ |
|||
*/\ |
|||
public static double fahrenheitToCelsius(double f) \{\ |
|||
return (f - 32.0) * 5.0 / 9.0;\ |
|||
\}\ |
|||
\ |
|||
public static void main(String[] args) \{\ |
|||
Scanner scanner = new Scanner(System.in);\ |
|||
\ |
|||
// \uc0\u25552 \u31034 \u29992 \u25143 \u36755 \u20837 \u65292 \u26684 \u24335 \u31034 \u20363 : "36.6 C" \u25110 "97 F"\ |
|||
System.out.print("\uc0\u35831 \u36755 \u20837 \u35201 \u36716 \u25442 \u30340 \u28201 \u24230 \u19982 \u21333 \u20301 \u65288 \u20363 \u22914 36.6 C \u25110 97 F\u65289 : ");\ |
|||
String s = scanner.nextLine().trim();\ |
|||
\ |
|||
if (s.isEmpty()) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u20026 \u31354 \u65292 \u31243 \u24207 \u36864 \u20986 \u12290 ");\ |
|||
return;\ |
|||
\}\ |
|||
\ |
|||
String[] parts = s.split("\\\\s+"); // \uc0\u25353 \u20219 \u24847 \u31354 \u30333 \u23383 \u31526 \u20998 \u21106 \ |
|||
double value;\ |
|||
String unit;\ |
|||
\ |
|||
try \{\ |
|||
// \uc0\u20801 \u35768 \u29992 \u25143 \u36755 \u20837 \u20004 \u20010 \u37096 \u20998 : \u25968 \u20540 \u19982 \u21333 \u20301 \ |
|||
value = Double.parseDouble(parts[0]);\ |
|||
unit = (parts.length > 1) ? parts[1].toUpperCase() : "C";\ |
|||
\} catch (Exception e) \{\ |
|||
System.out.println("\uc0\u36755 \u20837 \u35299 \u26512 \u22833 \u36133 \u65292 \u35831 \u25353 \u31034 \u20363 \u36755 \u20837 \u25968 \u20540 \u19982 \u21333 \u20301 \u65292 \u20363 \u22914 : 36.6 C");\ |
|||
return;\ |
|||
\}\ |
|||
\ |
|||
if (unit.startsWith("C")) \{\ |
|||
// \uc0\u20174 \u25668 \u27663 \u24230 \u36716 \u25442 \u20026 \u21326 \u27663 \u24230 \ |
|||
double f = celsiusToFahrenheit(value);\ |
|||
System.out.printf("%.1f \'b0C = %.2f \'b0F%n", value, f);\ |
|||
\} else if (unit.startsWith("F")) \{\ |
|||
// \uc0\u20174 \u21326 \u27663 \u24230 \u36716 \u25442 \u20026 \u25668 \u27663 \u24230 \ |
|||
double c = fahrenheitToCelsius(value);\ |
|||
System.out.printf("%.1f \'b0F = %.2f \'b0C%n", value, c);\ |
|||
\} else \{\ |
|||
System.out.println("\uc0\u26410 \u30693 \u21333 \u20301 \u65292 \u35831 \u20351 \u29992 C \u25110 F\u12290 ");\ |
|||
\}\ |
|||
\ |
|||
scanner.close();\ |
|||
\}\ |
|||
\}} |
|||
@ -0,0 +1,67 @@ |
|||
import java.util.Scanner; |
|||
|
|||
/** |
|||
* TemperatureConverter |
|||
* 支持摄氏度(C)与华氏度(F)之间互转 |
|||
*/ |
|||
public class TemperatureConverter { |
|||
|
|||
/** |
|||
* 将摄氏度转换为华氏度 |
|||
* @param c 摄氏温度 |
|||
* @return 对应的华氏温度 |
|||
*/ |
|||
public static double celsiusToFahrenheit(double c) { |
|||
return c * 9.0 / 5.0 + 32.0; |
|||
} |
|||
|
|||
/** |
|||
* 将华氏度转换为摄氏度 |
|||
* @param f 华氏温度 |
|||
* @return 对应的摄氏温度 |
|||
*/ |
|||
public static double fahrenheitToCelsius(double f) { |
|||
return (f - 32.0) * 5.0 / 9.0; |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
Scanner scanner = new Scanner(System.in); |
|||
|
|||
// 提示用户输入,格式示例:"36.6 C" 或 "97 F" |
|||
System.out.print("请输入要转换的温度与单位(例如 36.6 C 或 97 F):"); |
|||
String input = scanner.nextLine().trim(); |
|||
|
|||
if (input.isEmpty()) { |
|||
System.out.println("输入为空,程序退出。"); |
|||
scanner.close(); |
|||
return; |
|||
} |
|||
|
|||
String[] parts = input.split("\\s+"); |
|||
|
|||
try { |
|||
// 解析数值和单位 |
|||
double value = Double.parseDouble(parts[0]); |
|||
String unit = parts.length > 1 ? parts[1].toUpperCase() : "C"; |
|||
|
|||
if (unit.startsWith("C")) { |
|||
// 从摄氏度转换为华氏度 |
|||
double f = celsiusToFahrenheit(value); |
|||
System.out.printf("%.2f °C = %.2f °F%n", value, f); |
|||
} else if (unit.startsWith("F")) { |
|||
// 从华氏度转换为摄氏度 |
|||
double c = fahrenheitToCelsius(value); |
|||
System.out.printf("%.2f °F = %.2f °C%n", value, c); |
|||
} else { |
|||
System.out.println("未知单位,请使用 C 或 F。"); |
|||
} |
|||
|
|||
} catch (NumberFormatException e) { |
|||
System.out.println("输入解析失败,请按示例输入数值与单位,例如:36.6 C"); |
|||
} catch (Exception e) { |
|||
System.out.println("发生错误:" + e.getMessage()); |
|||
} finally { |
|||
scanner.close(); |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,43 @@ |
|||
{\rtf1\ansi\ansicpg936\cocoartf2822 |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} |
|||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset134 PingFangSC-Regular;} |
|||
{\colortbl;\red255\green255\blue255;} |
|||
{\*\expandedcolortbl;;} |
|||
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0 |
|||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 |
|||
|
|||
\f0\fs24 \cf0 AI \uc0\u21327 \u21161 \u35760 \u24405 \ |
|||
\f0\fs24 \cf0 AI |
|||
\f1 \'d0\'ad\'d6\'fa\'bf\'aa\'b7\'a2\'ce\'c2\'b6\'c8\'d7\'aa\'bb\'bb\'b3\'cc\'d0\'f2\'bc\'c7\'c2\'bc |
|||
\f0 \ |
|||
\ |
|||
\uc0\u26412 \u27425 \u20316 \u19994 \u20013 \u25105 \u20511 \u21161 AI\u36741 \u21161 \u23436 \u25104 \u20195 \u30721 \u35821 \u27861 \u32416 \u38169 \u12289 \u36923 \u36753 \u20248 \u21270 \u19982 \u26684 \u24335 \u35268 \u33539 \u25972 \u29702 \u12290 \u31227 \u26893 \u36807 \u31243 \u20013 \u65292 \u25105 \u33258 \u34892 \u23436 \u25104 \u20102 \u25972 \u20307 \u31243 \u24207 \u36923 \u36753 \u35774 \u35745 \u12289 \u28201 \u24230 \u36716 \u25442 \u20844 \u24335 \u32534 \u20889 \u19982 \u20132 \u20114 \u27969 \u31243 \u26500 \u24605 \u65292 \u21033 \u29992 AI\u24110 \u21161 \u26816 \u26597 Java\u35821 \u27861 \u32454 \u33410 \u12289 \u23436 \u21892 \u20195 \u30721 \u27880 \u37322 \u26684 \u24335 \u12289 \u25972 \u29702 \u32534 \u35793 \u36816 \u34892 \u25351 \u20196 \u20197 \u21450 \u35268 \u25972 README\u25991 \u26723 \u25490 \u29256 \u65292 \u20445 \u35777 \u31243 \u24207 \u33021 \u22815 \u31283 \u23450 \u36816 \u34892 \u12289 \u31526 \u21512 \u35838 \u31243 \u20316 \u19994 \u25552 \u20132 \u26631 \u20934 \u12290 } |
|||
|
|||
\f1 \'d4\'da\'bf\'aa\'b7\'a2\'ce\'c2\'b6\'c8\'d7\'aa\'bb\'bb\'b3\'cc\'d0\'f2\'ca\'b1\'a3\'ac |
|||
\f0 AI |
|||
\f1 \'cc\'e1\'b9\'a9\'c1\'cb\'c7\'e5\'ce\'fa\'a1\'a2\'b8\'df\'d0\'a7\'b5\'c4\'d6\'a7\'b3\'d6\'a3\'ac\'d6\'f7\'d2\'aa\'cc\'e5\'cf\'d6\'d4\'da\'d2\'d4\'cf\'c2\'bc\'b8\'b8\'f6\'b7\'bd\'c3\'e6\'a3\'ba |
|||
\f0 \ |
|||
\ |
|||
1.\'a0 |
|||
\f1 \'d0\'e8\'c7\'f3\'ca\'e1\'c0\'ed\'a3\'ba\'c3\'f7\'c8\'b7\'b3\'cc\'d0\'f2\'d0\'e8\'d6\'a7\'b3\'d6\'c9\'e3\'ca\'cf |
|||
\f0 / |
|||
\f1 \'bb\'aa\'ca\'cf\'bb\'a5\'d7\'aa\'a1\'a2\'c3\'fc\'c1\'ee\'d0\'d0\'b2\'ce\'ca\'fd\'ba\'cd\'c5\'fa\'c1\'bf\'ce\'c4\'bc\'fe\'d7\'aa\'bb\'bb\'c8\'fd\'d6\'d6\'c4\'a3\'ca\'bd\'a3\'ac\'b2\'a2\'b9\'e6\'bb\'ae\'c1\'cb\'c4\'a3\'bf\'e9\'bb\'af\'bd\'e1\'b9\'b9\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
2.\'a0 |
|||
\f1 \'b4\'fa\'c2\'eb\'c9\'fa\'b3\'c9\'a3\'ba\'c9\'fa\'b3\'c9\'c1\'cb\'cd\'ea\'d5\'fb\'b5\'c4 |
|||
\f0 Java |
|||
\f1 \'b4\'fa\'c2\'eb\'bf\'f2\'bc\'dc\'a3\'ac\'b0\'fc\'c0\'a8\'ba\'cb\'d0\'c4\'d7\'aa\'bb\'bb\'ba\'af\'ca\'fd\'a1\'a2\'cd\'b3\'d2\'bb\'bd\'e2\'ce\'f6\'c2\'df\'bc\'ad\'ba\'cd\'c5\'fa\'c1\'bf\'b4\'a6\'c0\'ed\'c4\'a3\'bf\'e9\'a3\'ac\'b2\'a2\'bc\'d3\'c8\'eb\'c1\'cb\'d2\'ec\'b3\'a3\'b4\'a6\'c0\'ed\'a3\'ac\'cc\'e1\'c9\'fd\'c1\'cb\'b3\'cc\'d0\'f2\'bd\'a1\'d7\'b3\'d0\'d4\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
3.\'a0 |
|||
\f1 \'bb\'b7\'be\'b3\'d6\'b8\'b5\'bc\'a3\'ba\'d5\'eb\'b6\'d4 |
|||
\f0 Mac |
|||
\f1 \'bb\'b7\'be\'b3\'a3\'ac\'cc\'e1\'b9\'a9\'c1\'cb\'b1\'e0\'d2\'eb\'a1\'a2\'d4\'cb\'d0\'d0\'ba\'cd |
|||
\f0 Git |
|||
\f1 \'cc\'e1\'bd\'bb\'b5\'c4\'b7\'d6\'b2\'bd\'b2\'d9\'d7\'f7\'a3\'ac\'bd\'e2\'be\'f6\'c1\'cb\'ce\'c4\'bc\'fe\'c2\'b7\'be\'b6\'a1\'a2\'d6\'d5\'b6\'cb\'b1\'a8\'b4\'ed\'b5\'c8\'ce\'ca\'cc\'e2\'a1\'a3 |
|||
\f0 \ |
|||
\ |
|||
4.\'a0 |
|||
\f1 \'b2\'d6\'bf\'e2\'d3\'c5\'bb\'af\'a3\'ba\'bd\'a8\'d2\'e9\'cd\'a8\'b9\'fd |
|||
\f0 \'a0.gitignore\'a0 |
|||
\f1 \'ce\'c4\'bc\'fe\'b9\'e6\'b7\'b6\'cc\'e1\'bd\'bb\'a3\'ac\'b1\'dc\'c3\'e2\'ce\'de\'b9\'d8\'ce\'c4\'bc\'fe\'b8\'c9\'c8\'c5\'a3\'ac\'b2\'a2\'d6\'b8\'b5\'bc\'c8\'e7\'ba\'ce\'d4\'da |
|||
\f0 README |
|||
\f1 \'d6\'d0\'d5\'b9\'ca\'be\'b4\'fa\'c2\'eb\'ba\'cd\'d4\'cb\'d0\'d0\'bd\'d8\'cd\'bc\'a1\'a3} |
|||
|
Before Width: | Height: | Size: 656 KiB After Width: | Height: | Size: 1.4 MiB |
@ -1,71 +0,0 @@ |
|||
|
|||
|
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import com.example.datacollect.repository.ArticleRepository; |
|||
import com.example.datacollect.strategy.CrawlStrategy; |
|||
import com.example.datacollect.strategy.StrategyFactory; |
|||
import com.example.datacollect.view.ConsoleView; |
|||
import org.jsoup.Jsoup; |
|||
import org.jsoup.nodes.Document; |
|||
|
|||
import java.util.List; |
|||
|
|||
public class AnalyzeCommand implements Command { |
|||
private final ConsoleView view; |
|||
private final StrategyFactory strategyFactory; |
|||
|
|||
public AnalyzeCommand(ConsoleView view, StrategyFactory strategyFactory) { |
|||
this.view = view; |
|||
this.strategyFactory = strategyFactory; |
|||
} |
|||
|
|||
@Override |
|||
public String getName() { |
|||
return "analyze"; |
|||
} |
|||
|
|||
@Override |
|||
public void execute(String[] args, ArticleRepository repository) { |
|||
if (args.length < 2) { |
|||
view.printError("Usage: analyze <url>"); |
|||
return; |
|||
} |
|||
String url = args[1]; |
|||
|
|||
CrawlStrategy strategy = strategyFactory.getStrategy(url); |
|||
if (strategy == null) { |
|||
view.printError("No strategy found for: " + url); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
view.printInfo("Analyzing: " + url); |
|||
view.printInfo("Using strategy: " + strategy.getClass().getSimpleName()); |
|||
|
|||
Document doc = Jsoup.connect(url).get(); |
|||
List<Article> articles = strategy.parse(url, doc); |
|||
|
|||
view.printSuccess("=== Analysis Statistics ==="); |
|||
view.printSuccess("URL: " + url); |
|||
view.printSuccess("Strategy: " + strategy.getClass().getSimpleName()); |
|||
view.printSuccess("Articles found: " + articles.size()); |
|||
|
|||
if (!articles.isEmpty()) { |
|||
view.printSuccess("Sample titles:"); |
|||
int count = Math.min(3, articles.size()); |
|||
for (int i = 0; i < count; i++) { |
|||
view.printSuccess(" - " + articles.get(i).getTitle()); |
|||
} |
|||
if (articles.size() > 3) { |
|||
view.printSuccess(" ... and " + (articles.size() - 3) + " more"); |
|||
} |
|||
} |
|||
|
|||
view.printSuccess("Note: Articles were NOT stored in repository"); |
|||
|
|||
} catch (Exception e) { |
|||
view.printError("Failed to analyze: " + e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
|
|||
|
|||
import com.example.datacollect.model.Article; |
|||
import org.jsoup.nodes.Document; |
|||
import java.util.List; |
|||
|
|||
public interface CrawlStrategy { |
|||
List<Article> parse(String url, Document doc); |
|||
boolean supports(String url); |
|||
|
|||
default int getPriority() { |
|||
return 0; |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
// 先定义爬虫策略接口(必须先建这个,否则ASiteCrawlStrategy会报错)
|
|||
public interface ASiteCrawlStrategy { |
|||
void crawl(String url); |
|||
} |
|||
|
|||
// A网站爬虫策略类(实现上面的接口)
|
|||
class ASiteCrawlStrategyImpl implements CrawlStrategy { |
|||
@Override |
|||
public void crawl(String url) { |
|||
// 先写简单逻辑:打印爬取信息
|
|||
System.out.println("正在爬取A网站:" + url); |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
// 根异常:继承RuntimeException(Unchecked异常),爬虫项目所有异常都继承它
|
|||
public class CrawlException extends RuntimeException { |
|||
// 构造器1:只传错误信息
|
|||
public CrawlException(String message) { |
|||
super(message); |
|||
} |
|||
|
|||
// 构造器2:传错误信息+原始异常(方便排查根因)
|
|||
public CrawlException(String message, Throwable cause) { |
|||
super(message, cause); |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
// 网络异常:继承根异常CrawlException
|
|||
public class NetworkException extends CrawlException { |
|||
public NetworkException(String message) { |
|||
// 给错误信息加前缀,方便定位
|
|||
super("网络请求失败:" + message); |
|||
} |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
// 解析异常:继承根异常CrawlException
|
|||
public class ParseException extends CrawlException { |
|||
public ParseException(String message) { |
|||
super("数据解析失败:" + message); |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -1,8 +0,0 @@ |
|||
package com.example.datacollect; |
|||
|
|||
// 不支持的网站异常:继承根异常CrawlException
|
|||
public class UnsupportedSiteException extends CrawlException { |
|||
public UnsupportedSiteException(String siteKey) { |
|||
super("不支持的网站类型:" + siteKey); |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
public class DataCleaner { |
|||
public static void main(String[] args) { |
|||
int[] sensorData = {85, -5, 92, 0, 105, 999, 88, 76}; |
|||
|
|||
int validSum = 0; // 有效数据总和
|
|||
int validCount = 0; // 有效数据个数
|
|||
|
|||
// 流程控制代码
|
|||
for (int data : sensorData) { |
|||
// 致命错误:传感器掉线
|
|||
if (data == 999) { |
|||
System.out.println("致命错误:传感器掉线,终止处理"); |
|||
break; |
|||
} |
|||
|
|||
// 无效数据:0或负数,或大于100(且不是999)
|
|||
if (data <= 0 || data > 100) { |
|||
System.out.println("警告:发现越界数据 [" + data + "],已跳过"); |
|||
continue; |
|||
} |
|||
|
|||
// 正常范围数据(1-100)
|
|||
validSum += data; |
|||
validCount++; |
|||
} |
|||
|
|||
// 最终输出
|
|||
if (validCount > 0) { |
|||
// 注意:避免整数除法,先将validSum转为double
|
|||
double average = (double) validSum / validCount; |
|||
System.out.println("有效数据的平均值:" + average); |
|||
} else { |
|||
System.out.println("无有效数据"); |
|||
} |
|||
} |
|||
} |
|||
@ -1,529 +0,0 @@ |
|||
import javax.imageio.ImageIO; |
|||
import java.awt.*; |
|||
import java.awt.image.BufferedImage; |
|||
import java.io.File; |
|||
import java.net.URI; |
|||
import java.net.http.HttpClient; |
|||
import java.net.http.HttpRequest; |
|||
import java.net.http.HttpResponse; |
|||
import java.util.*; |
|||
import java.util.List; |
|||
import java.util.regex.Matcher; |
|||
import java.util.regex.Pattern; |
|||
|
|||
// ========== 1. 泛型接口:定义通用数据处理行为 ==========
|
|||
/** |
|||
* 通用数据爬虫接口(泛型:T-爬取数据类型,K-数据唯一标识类型) |
|||
*/ |
|||
interface DataCrawler<T, K> { |
|||
// 爬取数据
|
|||
T crawlData(K key) throws Exception; |
|||
// 解析数据
|
|||
Map<String, Object> parseData(T rawData); |
|||
// 保存数据
|
|||
void saveData(K key, Map<String, Object> data); |
|||
} |
|||
|
|||
/** |
|||
* 通用数据可视化接口(泛型:T-可视化数据类型) |
|||
*/ |
|||
interface DataVisualizer<T> { |
|||
void generateVisualization(String title, T data); |
|||
} |
|||
|
|||
// ========== 2. 抽象泛型父类:爬虫基类 ==========
|
|||
abstract class AbstractDataCrawler<T, K> implements DataCrawler<T, K> { |
|||
// 泛型集合:存储爬取的原始数据(K-标识,T-原始数据)
|
|||
protected Map<K, T> rawDataMap = new HashMap<>(); |
|||
// 泛型集合:存储解析后的结构化数据(K-标识,Map-结构化数据)
|
|||
protected Map<K, Map<String, Object>> parsedDataMap = new LinkedHashMap<>(); |
|||
// 泛型集合:存储爬取失败的标识
|
|||
protected List<K> failedKeys = new LinkedList<>(); |
|||
|
|||
// 通用HTTP请求方法(泛型返回值)
|
|||
protected String doHttpGet(String url) throws Exception { |
|||
HttpClient client = HttpClient.newHttpClient(); |
|||
HttpRequest request = HttpRequest.newBuilder() |
|||
.uri(URI.create(url)) |
|||
.GET() |
|||
.build(); |
|||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); |
|||
if (response.statusCode() != 200) { |
|||
throw new RuntimeException("HTTP请求失败,状态码:" + response.statusCode()); |
|||
} |
|||
return response.body(); |
|||
} |
|||
|
|||
// 通用失败记录方法
|
|||
protected void recordFailure(K key, Exception e) { |
|||
failedKeys.add(key); |
|||
System.err.println("❌ 爬取" + key + "失败:" + e.getMessage()); |
|||
} |
|||
|
|||
// 泛型方法:获取解析后的数据
|
|||
public <V> V getParsedValue(K key, String field, Class<V> type) { |
|||
if (parsedDataMap.containsKey(key) && parsedDataMap.get(key).containsKey(field)) { |
|||
Object value = parsedDataMap.get(key).get(field); |
|||
if (type.isInstance(value)) { |
|||
return type.cast(value); |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
// 抽象方法:获取API地址(子类实现)
|
|||
protected abstract String getApiUrl(K key); |
|||
} |
|||
|
|||
// ========== 3. 天气爬虫子类(泛型实现) ==========
|
|||
class WeatherCrawler extends AbstractDataCrawler<String, String> implements DataVisualizer<Map<String, List<?>>> { |
|||
// 城市经纬度映射(泛型集合)
|
|||
private Map<String, String[]> cityLatLonMap = new HashMap<>(); |
|||
|
|||
// 初始化城市数据
|
|||
public WeatherCrawler() { |
|||
cityLatLonMap.put("西安", new String[]{"34.2644", "108.9497"}); |
|||
cityLatLonMap.put("成都", new String[]{"30.5728", "104.0668"}); |
|||
cityLatLonMap.put("兰州", new String[]{"36.0611", "103.8343"}); |
|||
cityLatLonMap.put("乌鲁木齐", new String[]{"43.8256", "87.6168"}); |
|||
} |
|||
|
|||
@Override |
|||
public String crawlData(String cityName) throws Exception { |
|||
if (!cityLatLonMap.containsKey(cityName)) { |
|||
throw new IllegalArgumentException("未配置城市:" + cityName + "的经纬度"); |
|||
} |
|||
String url = getApiUrl(cityName); |
|||
System.out.println("🌐 正在爬取" + cityName + "天气数据:" + url); |
|||
String rawData = doHttpGet(url); |
|||
rawDataMap.put(cityName, rawData); |
|||
return rawData; |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Object> parseData(String rawData) { |
|||
Map<String, Object> parsedData = new HashMap<>(); |
|||
try { |
|||
List<String> times = parseTimes(rawData); |
|||
List<Double> temps = parseTemperatures(rawData); |
|||
parsedData.put("times", times); |
|||
parsedData.put("temps", temps); |
|||
parsedData.put("minTemp", temps.stream().mapToDouble(Double::doubleValue).min().orElse(0)); |
|||
parsedData.put("maxTemp", temps.stream().mapToDouble(Double::doubleValue).max().orElse(0)); |
|||
} catch (Exception e) { |
|||
System.err.println("❌ 解析天气数据失败:" + e.getMessage()); |
|||
} |
|||
return parsedData; |
|||
} |
|||
|
|||
@Override |
|||
public void saveData(String cityName, Map<String, Object> data) { |
|||
parsedDataMap.put(cityName, data); |
|||
System.out.println("💾 " + cityName + "天气数据已保存,解析字段数:" + data.size()); |
|||
} |
|||
|
|||
@Override |
|||
protected String getApiUrl(String cityName) { |
|||
String[] latLon = cityLatLonMap.get(cityName); |
|||
return String.format( |
|||
"https://api.open-meteo.com/v1/forecast?latitude=%s&longitude=%s&hourly=temperature_2m&past_days=1&forecast_days=3", |
|||
latLon[0], latLon[1] |
|||
); |
|||
} |
|||
|
|||
// 解析时间(复用原有逻辑)
|
|||
private List<String> parseTimes(String json) { |
|||
List<String> times = new ArrayList<>(); |
|||
try { |
|||
Pattern pattern = Pattern.compile("\"time\":\\[([^\\]]+)\\]"); |
|||
Matcher matcher = pattern.matcher(json); |
|||
if (matcher.find()) { |
|||
String timeStr = matcher.group(1); |
|||
String[] timeArray = timeStr.split(","); |
|||
for (String t : timeArray) { |
|||
t = t.trim().replace("\"", ""); |
|||
if (!t.isEmpty()) { |
|||
if (t.contains("T")) { |
|||
String date = t.substring(5, 10); |
|||
String time = t.substring(11, 16); |
|||
times.add(date + "\n" + time); |
|||
} else { |
|||
times.add(t); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("❌ 解析时间失败:" + e.getMessage()); |
|||
} |
|||
return times; |
|||
} |
|||
|
|||
// 解析温度(复用原有逻辑)
|
|||
private List<Double> parseTemperatures(String json) { |
|||
List<Double> temps = new ArrayList<>(); |
|||
try { |
|||
Pattern pattern = Pattern.compile("\"temperature_2m\":\\[([^\\]]+)\\]"); |
|||
Matcher matcher = pattern.matcher(json); |
|||
if (matcher.find()) { |
|||
String tempStr = matcher.group(1); |
|||
String[] tempArray = tempStr.split(","); |
|||
for (String t : tempArray) { |
|||
t = t.trim(); |
|||
if (!t.isEmpty()) { |
|||
try { |
|||
temps.add(Double.parseDouble(t)); |
|||
} catch (NumberFormatException e) { |
|||
System.err.println("⚠️ 无法解析温度值: " + t); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
System.err.println("❌ 解析温度失败:" + e.getMessage()); |
|||
} |
|||
return temps; |
|||
} |
|||
|
|||
// 天气数据可视化(泛型实现)
|
|||
@Override |
|||
public void generateVisualization(String cityName, Map<String, List<?>> data) { |
|||
List<String> times = (List<String>) data.get("times"); |
|||
List<Double> temps = (List<Double>) data.get("temps"); |
|||
if (times == null || temps == null || times.isEmpty() || temps.isEmpty()) { |
|||
System.out.println("⚠️ " + cityName + " 数据无效,跳过绘图"); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
int width = 1400; |
|||
int height = 700; |
|||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); |
|||
Graphics2D g2d = image.createGraphics(); |
|||
|
|||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); |
|||
g2d.setColor(Color.WHITE); |
|||
g2d.fillRect(0, 0, width, height); |
|||
|
|||
int marginLeft = 120; |
|||
int marginRight = 60; |
|||
int marginTop = 80; |
|||
int marginBottom = 120; |
|||
|
|||
int chartWidth = width - marginLeft - marginRight; |
|||
int chartHeight = height - marginTop - marginBottom; |
|||
|
|||
double minTemp = temps.stream().mapToDouble(Double::doubleValue).min().orElse(0); |
|||
double maxTemp = temps.stream().mapToDouble(Double::doubleValue).max().orElse(0); |
|||
double tempRange = maxTemp - minTemp; |
|||
|
|||
// 画网格
|
|||
g2d.setColor(Color.LIGHT_GRAY); |
|||
g2d.setStroke(new BasicStroke(1)); |
|||
int numYLines = 10; |
|||
for (int i = 0; i <= numYLines; i++) { |
|||
int y = marginTop + (chartHeight * i / numYLines); |
|||
g2d.drawLine(marginLeft, y, marginLeft + chartWidth, y); |
|||
double temp = maxTemp - (tempRange * i / numYLines); |
|||
String label = String.format("%.1f°C", temp); |
|||
g2d.setColor(Color.BLACK); |
|||
g2d.setFont(new Font("Arial", Font.PLAIN, 12)); |
|||
g2d.drawString(label, marginLeft - 50, y + 4); |
|||
g2d.setColor(Color.LIGHT_GRAY); |
|||
} |
|||
|
|||
// 画坐标轴
|
|||
g2d.setColor(Color.BLACK); |
|||
g2d.setStroke(new BasicStroke(2)); |
|||
g2d.drawLine(marginLeft, marginTop, marginLeft, marginTop + chartHeight); |
|||
g2d.drawLine(marginLeft, marginTop + chartHeight, marginLeft + chartWidth, marginTop + chartHeight); |
|||
|
|||
// 标题
|
|||
g2d.setFont(new Font("Arial", Font.BOLD, 20)); |
|||
String title = cityName + " 逐小时温度变化(过去1天+未来3天)"; |
|||
g2d.drawString(title, width / 2 - 250, 45); |
|||
|
|||
// 轴标签
|
|||
g2d.setFont(new Font("Arial", Font.PLAIN, 14)); |
|||
g2d.drawString("时间", width / 2 - 20, height - 40); |
|||
|
|||
Graphics2D g2dRotated = (Graphics2D) g2d.create(); |
|||
g2dRotated.rotate(-Math.PI / 2); |
|||
g2dRotated.drawString("温度 (°C)", -height / 2, 35); |
|||
g2dRotated.dispose(); |
|||
|
|||
// 画数据
|
|||
if (temps.size() > 1) { |
|||
int[] xPoints = new int[temps.size()]; |
|||
int[] yPoints = new int[temps.size()]; |
|||
|
|||
for (int i = 0; i < temps.size(); i++) { |
|||
int x = marginLeft + (chartWidth * i / (temps.size() - 1)); |
|||
int y = marginTop + chartHeight - (int) ((temps.get(i) - minTemp) * chartHeight / tempRange); |
|||
xPoints[i] = x; |
|||
yPoints[i] = y; |
|||
} |
|||
|
|||
g2d.setColor(new Color(255, 0, 0, 180)); |
|||
g2d.setStroke(new BasicStroke(2.5f)); |
|||
for (int i = 0; i < temps.size() - 1; i++) { |
|||
g2d.drawLine(xPoints[i], yPoints[i], xPoints[i + 1], yPoints[i + 1]); |
|||
} |
|||
|
|||
g2d.setColor(Color.RED); |
|||
for (int i = 0; i < temps.size(); i++) { |
|||
g2d.fillOval(xPoints[i] - 3, yPoints[i] - 3, 6, 6); |
|||
if (i % 12 == 0) { |
|||
g2d.setColor(Color.BLUE); |
|||
g2d.setFont(new Font("Arial", Font.PLAIN, 10)); |
|||
g2d.drawString(String.format("%.1f", temps.get(i)), xPoints[i] + 5, yPoints[i] - 5); |
|||
g2d.setColor(Color.RED); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// X轴标签
|
|||
g2d.setColor(Color.BLACK); |
|||
g2d.setFont(new Font("Arial", Font.PLAIN, 9)); |
|||
int step = Math.max(1, times.size() / 15); |
|||
for (int i = 0; i < times.size(); i += step) { |
|||
int x = marginLeft + (chartWidth * i / (times.size() - 1)); |
|||
int y = marginTop + chartHeight + 15; |
|||
String label = times.get(i); |
|||
if (label.contains("\n")) { |
|||
String[] lines = label.split("\n"); |
|||
g2d.drawString(lines[0], x - 20, y); |
|||
g2d.drawString(lines[1], x - 20, y + 12); |
|||
} else if (label.length() > 10) { |
|||
g2d.drawString(label.substring(0, 10), x - 20, y + 5); |
|||
} else { |
|||
g2d.drawString(label, x - 15, y + 5); |
|||
} |
|||
} |
|||
|
|||
g2d.dispose(); |
|||
|
|||
String fileName = cityName + "_温度图.png"; |
|||
File outputFile = new File(fileName); |
|||
ImageIO.write(image, "PNG", outputFile); |
|||
System.out.println("💾 " + cityName + "图表已保存为:" + outputFile.getAbsolutePath()); |
|||
|
|||
} catch (Exception e) { |
|||
System.err.println("❌ 生成" + cityName + "图表失败:" + e.getMessage()); |
|||
e.printStackTrace(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ========== 4. 城市特色爬虫子类(扩展新类型爬虫) ==========
|
|||
class CityInfoCrawler extends AbstractDataCrawler<Map<String, String>, String> { |
|||
// 模拟城市特色数据(实际可替换为真实爬虫逻辑)
|
|||
private Map<String, Map<String, String>> cityInfoSource = new HashMap<>(); |
|||
|
|||
public CityInfoCrawler() { |
|||
// 初始化城市特色数据
|
|||
Map<String, String> xiAnInfo = new HashMap<>(); |
|||
xiAnInfo.put("别名", "十三朝古都"); |
|||
xiAnInfo.put("地标", "兵马俑、大雁塔"); |
|||
xiAnInfo.put("美食", "肉夹馍、泡馍"); |
|||
xiAnInfo.put("经纬度", "34.2644, 108.9497"); |
|||
cityInfoSource.put("西安", xiAnInfo); |
|||
|
|||
Map<String, String> chengDuInfo = new HashMap<>(); |
|||
chengDuInfo.put("别名", "天府之国"); |
|||
chengDuInfo.put("地标", "大熊猫基地、宽窄巷子"); |
|||
chengDuInfo.put("美食", "火锅、串串"); |
|||
chengDuInfo.put("经纬度", "30.5728, 104.0668"); |
|||
cityInfoSource.put("成都", chengDuInfo); |
|||
|
|||
Map<String, String> lanZhouInfo = new HashMap<>(); |
|||
lanZhouInfo.put("别名", "黄河之都"); |
|||
lanZhouInfo.put("地标", "黄河铁桥、白塔山"); |
|||
lanZhouInfo.put("美食", "牛肉面、甜醅子"); |
|||
lanZhouInfo.put("经纬度", "36.0611, 103.8343"); |
|||
cityInfoSource.put("兰州", lanZhouInfo); |
|||
|
|||
Map<String, String> wuLuMuQiInfo = new HashMap<>(); |
|||
wuLuMuQiInfo.put("别名", "亚心之都"); |
|||
wuLuMuQiInfo.put("地标", "天山、国际大巴扎"); |
|||
wuLuMuQiInfo.put("美食", "羊肉串、手抓饭"); |
|||
wuLuMuQiInfo.put("经纬度", "43.8256, 87.6168"); |
|||
cityInfoSource.put("乌鲁木齐", wuLuMuQiInfo); |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, String> crawlData(String cityName) throws Exception { |
|||
if (!cityInfoSource.containsKey(cityName)) { |
|||
throw new IllegalArgumentException("未找到" + cityName + "的特色数据"); |
|||
} |
|||
System.out.println("🌐 正在爬取" + cityName + "城市特色数据"); |
|||
Map<String, String> rawData = cityInfoSource.get(cityName); |
|||
rawDataMap.put(cityName, rawData); |
|||
return rawData; |
|||
} |
|||
|
|||
@Override |
|||
public Map<String, Object> parseData(Map<String, String> rawData) { |
|||
// 转换为通用Map结构(可扩展解析逻辑)
|
|||
Map<String, Object> parsedData = new HashMap<>(rawData); |
|||
// 新增解析字段:经纬度拆分
|
|||
String latLon = (String) rawData.get("经纬度"); |
|||
if (latLon != null) { |
|||
String[] latLonArr = latLon.split(","); |
|||
parsedData.put("纬度", Double.parseDouble(latLonArr[0].trim())); |
|||
parsedData.put("经度", Double.parseDouble(latLonArr[1].trim())); |
|||
} |
|||
return parsedData; |
|||
} |
|||
|
|||
@Override |
|||
public void saveData(String cityName, Map<String, Object> data) { |
|||
parsedDataMap.put(cityName, data); |
|||
System.out.println("💾 " + cityName + "城市特色数据已保存,解析字段数:" + data.size()); |
|||
} |
|||
|
|||
@Override |
|||
protected String getApiUrl(String key) { |
|||
// 模拟API地址(实际可替换为真实城市信息API)
|
|||
return "https://api.example.com/cityinfo?name=" + key; |
|||
} |
|||
|
|||
// 扩展方法:打印城市特色
|
|||
public void printCityInfo(String cityName) { |
|||
Map<String, Object> info = parsedDataMap.get(cityName); |
|||
if (info == null) { |
|||
System.out.println("⚠️ 未找到" + cityName + "的特色数据"); |
|||
return; |
|||
} |
|||
System.out.println("\n🏙️ 【" + cityName + "】城市特色"); |
|||
for (Map.Entry<String, Object> entry : info.entrySet()) { |
|||
System.out.println(" " + entry.getKey() + ":" + entry.getValue()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ========== 5. 爬虫平台类(统一管理多类型爬虫) ==========
|
|||
class CrawlerPlatform { |
|||
// 泛型集合:管理所有爬虫(K-爬虫类型标识,V-爬虫实例)
|
|||
private Map<String, DataCrawler<?, ?>> crawlerMap = new HashMap<>(); |
|||
// 泛型集合:管理所有可视化器
|
|||
private Map<String, DataVisualizer<?>> visualizerMap = new HashMap<>(); |
|||
|
|||
// 注册爬虫
|
|||
public <T, K> void registerCrawler(String crawlerType, DataCrawler<T, K> crawler) { |
|||
crawlerMap.put(crawlerType, crawler); |
|||
System.out.println("✅ 注册爬虫成功:" + crawlerType); |
|||
} |
|||
|
|||
// 注册可视化器
|
|||
public <T> void registerVisualizer(String visualizerType, DataVisualizer<T> visualizer) { |
|||
visualizerMap.put(visualizerType, visualizer); |
|||
System.out.println("✅ 注册可视化器成功:" + visualizerType); |
|||
} |
|||
|
|||
// 执行爬虫(泛型方法)
|
|||
@SuppressWarnings("unchecked") |
|||
public <T, K> void runCrawler(String crawlerType, K key) { |
|||
DataCrawler<T, K> crawler = (DataCrawler<T, K>) crawlerMap.get(crawlerType); |
|||
if (crawler == null) { |
|||
System.err.println("❌ 未找到爬虫:" + crawlerType); |
|||
return; |
|||
} |
|||
try { |
|||
// 爬取 -> 解析 -> 保存
|
|||
T rawData = crawler.crawlData(key); |
|||
Map<String, Object> parsedData = crawler.parseData(rawData); |
|||
crawler.saveData(key, parsedData); |
|||
} catch (Exception e) { |
|||
((AbstractDataCrawler<T, K>) crawler).recordFailure(key, e); |
|||
} |
|||
} |
|||
|
|||
// 执行可视化
|
|||
@SuppressWarnings("unchecked") |
|||
public <T> void runVisualization(String visualizerType, String title, T data) { |
|||
DataVisualizer<T> visualizer = (DataVisualizer<T>) visualizerMap.get(visualizerType); |
|||
if (visualizer == null) { |
|||
System.err.println("❌ 未找到可视化器:" + visualizerType); |
|||
return; |
|||
} |
|||
visualizer.generateVisualization(title, data); |
|||
} |
|||
|
|||
// 获取爬虫实例(泛型方法)
|
|||
@SuppressWarnings("unchecked") |
|||
public <T, K> DataCrawler<T, K> getCrawler(String crawlerType) { |
|||
return (DataCrawler<T, K>) crawlerMap.get(crawlerType); |
|||
} |
|||
|
|||
// 获取可视化器实例
|
|||
@SuppressWarnings("unchecked") |
|||
public <T> DataVisualizer<T> getVisualizer(String visualizerType) { |
|||
return (DataVisualizer<T>) visualizerMap.get(visualizerType); |
|||
} |
|||
|
|||
// 打印平台统计信息
|
|||
public void printPlatformStats() { |
|||
System.out.println("\n📊 爬虫平台统计信息"); |
|||
System.out.println(" 已注册爬虫数:" + crawlerMap.size()); |
|||
System.out.println(" 已注册可视化器数:" + visualizerMap.size()); |
|||
// 遍历爬虫统计数据
|
|||
for (Map.Entry<String, DataCrawler<?, ?>> entry : crawlerMap.entrySet()) { |
|||
AbstractDataCrawler<?, ?> crawler = (AbstractDataCrawler<?, ?>) entry.getValue(); |
|||
System.out.println(" " + entry.getKey() + ":爬取成功数=" + crawler.rawDataMap.size() + ",失败数=" + crawler.failedKeys.size()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// ========== 6. 主程序(平台入口) ==========
|
|||
public class WeatherMain { |
|||
public static void main(String[] args) { |
|||
System.out.println("🚀 启动通用爬虫平台...\n"); |
|||
|
|||
// 1. 初始化平台
|
|||
CrawlerPlatform platform = new CrawlerPlatform(); |
|||
|
|||
// 2. 注册爬虫和可视化器
|
|||
WeatherCrawler weatherCrawler = new WeatherCrawler(); |
|||
CityInfoCrawler cityInfoCrawler = new CityInfoCrawler(); |
|||
platform.registerCrawler("weather", weatherCrawler); |
|||
platform.registerCrawler("cityInfo", cityInfoCrawler); |
|||
platform.registerVisualizer("weatherChart", weatherCrawler); |
|||
|
|||
// 3. 定义待爬取的城市列表(泛型集合)
|
|||
List<String> cities = new ArrayList<>(Arrays.asList("西安", "成都", "兰州", "乌鲁木齐")); |
|||
|
|||
// 4. 执行城市特色爬虫
|
|||
System.out.println("\n========== 执行城市特色爬虫 =========="); |
|||
for (String city : cities) { |
|||
platform.runCrawler("cityInfo", city); |
|||
cityInfoCrawler.printCityInfo(city); |
|||
} |
|||
|
|||
// 5. 执行天气爬虫 + 可视化
|
|||
System.out.println("\n========== 执行天气爬虫 + 可视化 =========="); |
|||
for (String city : cities) { |
|||
platform.runCrawler("weather", city); |
|||
// 获取解析后的天气数据并可视化
|
|||
Map<String, Object> weatherData = weatherCrawler.parsedDataMap.get(city); |
|||
if (weatherData != null) { |
|||
Map<String, List<?>> visualData = new HashMap<>(); |
|||
visualData.put("times", (List<String>) weatherData.get("times")); |
|||
visualData.put("temps", (List<Double>) weatherData.get("temps")); |
|||
platform.runVisualization("weatherChart", city, visualData); |
|||
} |
|||
} |
|||
|
|||
// 6. 平台统计
|
|||
platform.printPlatformStats(); |
|||
|
|||
// 7. 泛型方法演示:获取指定类型的解析值
|
|||
System.out.println("\n========== 泛型方法演示 =========="); |
|||
Double xiAnMaxTemp = weatherCrawler.getParsedValue("西安", "maxTemp", Double.class); |
|||
String chengDuFood = cityInfoCrawler.getParsedValue("成都", "美食", String.class); |
|||
System.out.println(" 西安最高温度:" + xiAnMaxTemp + "°C"); |
|||
System.out.println(" 成都特色美食:" + chengDuFood); |
|||
|
|||
System.out.println("\n🎉 爬虫平台执行完毕!"); |
|||
} |
|||
} |
|||
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 56 KiB |
@ -1,67 +0,0 @@ |
|||
// 抽象图形类
|
|||
abstract class Shape { |
|||
public abstract double getArea(); |
|||
} |
|||
|
|||
// 圆形类
|
|||
class Circle extends Shape { |
|||
private double radius; |
|||
|
|||
public Circle(double radius) { |
|||
this.radius = radius; |
|||
} |
|||
|
|||
@Override |
|||
public double getArea() { |
|||
return Math.PI * radius * radius; |
|||
} |
|||
} |
|||
|
|||
// 矩形类
|
|||
class Rectangle extends Shape { |
|||
private double width; |
|||
private double height; |
|||
|
|||
public Rectangle(double width, double height) { |
|||
this.width = width; |
|||
this.height = height; |
|||
} |
|||
|
|||
@Override |
|||
public double getArea() { |
|||
return width * height; |
|||
} |
|||
} |
|||
|
|||
// 三角形类
|
|||
class Triangle extends Shape { |
|||
private double base; |
|||
private double height; |
|||
|
|||
public Triangle(double base, double height) { |
|||
this.base = base; |
|||
this.height = height; |
|||
} |
|||
|
|||
@Override |
|||
public double getArea() { |
|||
return 0.5 * base * height; |
|||
} |
|||
} |
|||
|
|||
// 工具类
|
|||
class ShapeUtil { |
|||
public void printArea(Shape shape) { |
|||
System.out.printf("面积:%.2f%n", shape.getArea()); |
|||
} |
|||
} |
|||
|
|||
// 主类(程序入口)
|
|||
public class Main { |
|||
public static void main(String[] args) { |
|||
ShapeUtil util = new ShapeUtil(); |
|||
util.printArea(new Circle(5)); |
|||
util.printArea(new Rectangle(4, 6)); |
|||
util.printArea(new Triangle(3, 4)); |
|||
} |
|||
} |
|||
@ -1,2 +0,0 @@ |
|||
反思: |
|||
这次实验做完,我对抽象类、继承和多态的理解比之前深了很多。一开始我觉得没必要写抽象类,直接给每个图形类写个计算面积的方法不就行了,后来发现这样写代码很散乱,要是以后加新图形,很容易忘记加计算方法,而用抽象类统一规范后,所有子类都必须实现 getArea() ,不容易出错,这才明白抽象类的核心作用是“定规矩”。 |
|||
@ -1,46 +0,0 @@ |
|||
一、实验目的 |
|||
|
|||
1. 掌握Java抽象类、继承和多态的核心用法。 |
|||
|
|||
2. 学会用统一的方式处理不同图形的面积计算,简化代码结构。 |
|||
|
|||
3. 能绘制基础的UML类图,理清类之间的继承与依赖关系。 |
|||
|
|||
4. 理解组合与继承的区别,能根据场景选择合适的代码复用方式。 |
|||
|
|||
二、实验内容 |
|||
|
|||
1. 定义抽象类 Shape ,声明抽象方法 getArea() ,规范所有图形的面积计算行为。 |
|||
|
|||
2. 创建 Circle 、 Rectangle 、 Triangle 三个类,继承 Shape 并各自重写 getArea() 方法,实现对应图形的面积计算逻辑。 |
|||
|
|||
3. 编写 ShapeUtil 工具类,通过 printArea() 方法统一打印图形面积,利用多态特性适配不同图形对象。 |
|||
|
|||
4. 编写 Main 类作为程序入口,创建各类图形对象,调用工具类测试功能,确保运行正常。 |
|||
|
|||
5. 用Mermaid工具绘制UML类图,清晰展示类的继承关系和依赖关系。 |
|||
|
|||
三,ai协助记录 |
|||
|
|||
1. 帮我捋清楚这次实验的整体思路,知道该先写抽象类 Shape ,再让图形类去继承它。 |
|||
|
|||
2. 写代码的时候,我哪里不会就问 AI,比如 Circle 、 Rectangle 这些类怎么写,还有 ShapeUtil 工具类的逻辑。 |
|||
|
|||
3. 我跑代码报错,比如找不到 main 方法、类名不一致,都是 AI 帮我分析问题,教我怎么改的。 |
|||
|
|||
4. 画类图的时候,AI 给我讲了类之间的继承箭头怎么画,还帮我调整了类图的结构,让我看得更明白。 |
|||
|
|||
5. 报告里“组合和继承”的区别,我一开始分不清,AI 用大白话给我解释,还帮我整理了对比的内容。 |
|||
四、组合与继承的回答 |
|||
|
|||
1. 继承(Inheritance) |
|||
|
|||
- 定义:是“is-a”的关系,比如圆形是一种图形、矩形是一种图形,子类通过 extends 关键字继承父类的属性和方法,并重写父类的抽象方法来实现专属逻辑。- 本次实验中的作用:用 Shape 作为父类,统一规范所有图形的 getArea() 方法,让不同图形类有统一的行为标准,后续新增图形(比如正方形、梯形)只需继承 Shape 并重写方法即可,不用重新定义规范,代码复用性很强。 |
|||
- 优点:代码结构清晰,能快速建立类的层级关系,统一管理子类的行为规范,适合“本质是同类事物”的场景。 |
|||
|
|||
2. 组合(Composition) - 定义:是“has-a”的关系,通过在一个类中引用另一个类的对象来实现功能复用,比如 ShapeUtil 中使用 Shape 对象,不是继承它,而是调用它的方法。 |
|||
- 本次实验中的体现: ShapeUtil 不继承 Shape ,而是通过 printArea(Shape shape) 方法接收 Shape 类型的参数,间接使用 Shape 子类的功能,这种依赖关系就是组合的一种简单形式。 |
|||
- 优点:降低类之间的耦合度,父类修改不会直接影响调用类,扩展更灵活,适合“一个类需要使用另一个类功能”的场景,而非本质从属关系。 |
|||
五、实验反思 |
|||
|
|||
这次实验做完,我对抽象类、继承和多态的理解比之前深了很多。一开始我觉得没必要写抽象类,直接给每个图形类写个计算面积的方法不就行了,后来发现这样写代码很散乱,要是以后加新图形,很容易忘记加计算方法,而用抽象类统一规范后,所有子类都必须实现 getArea() ,不容易出错,这才明白抽象类的核心作用是“定规矩”。 |
|||
|
Before Width: | Height: | Size: 498 KiB |
|
Before Width: | Height: | Size: 412 KiB |
@ -1,4 +0,0 @@ |
|||
· 帮我理清了多态中“编译看左边,运行看右边”的底层机制 |
|||
· 解释了为什么父类引用调用子类重写方法时会执行子类逻辑 |
|||
· 讲解了Java中public类与文件名的强制对应关系 |
|||
· 说明了单文件编写时非public类的正确写法 |
|||
@ -1,33 +0,0 @@ |
|||
public class Pair<K, V> { |
|||
private K key; |
|||
private V value; |
|||
|
|||
public Pair(K key, V value) { |
|||
this.key = key; |
|||
this.value = value; |
|||
} |
|||
|
|||
public K getKey() { return key; } |
|||
public void setKey(K key) { this.key = key; } |
|||
public V getValue() { return value; } |
|||
public void setValue(V value) { this.value = value; } |
|||
|
|||
// 交换键值的静态方法
|
|||
public static <K, V> Pair<V, K> swap(Pair<K, V> pair) { |
|||
return new Pair<>(pair.getValue(), pair.getKey()); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Pair{" + "key=" + key + ", value=" + value + '}'; |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
// 测试
|
|||
Pair<String, Integer> pair = new Pair<>("age", 18); |
|||
System.out.println("交换前:" + pair); |
|||
|
|||
Pair<Integer, String> swappedPair = Pair.swap(pair); |
|||
System.out.println("交换后:" + swappedPair); |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
class Shape { |
|||
public void draw() { |
|||
System.out.println("绘制一个形状"); |
|||
} |
|||
} |
|||
|
|||
class Circle extends Shape { |
|||
@Override |
|||
public void draw() { |
|||
System.out.println("绘制一个圆形"); |
|||
} |
|||
} |
|||
|
|||
class Rectangle extends Shape { |
|||
@Override |
|||
public void draw() { |
|||
System.out.println("绘制一个矩形"); |
|||
} |
|||
} |
|||
|
|||
public class ShapeTest { |
|||
public static void drawShape(Shape s) { |
|||
s.draw(); |
|||
} |
|||
|
|||
public static void main(String[] args) { |
|||
Shape shape1 = new Circle(); |
|||
Shape shape2 = new Rectangle(); |
|||
Shape shape3 = new Shape(); |
|||
|
|||
drawShape(shape1); |
|||
drawShape(shape2); |
|||
drawShape(shape3); |
|||
} |
|||
} |
|||
@ -1,55 +0,0 @@ |
|||
// 接口
|
|||
interface USB { |
|||
void open(); |
|||
void close(); |
|||
} |
|||
|
|||
// Mouse类实现USB接口
|
|||
class Mouse implements USB { |
|||
@Override |
|||
public void open() { |
|||
System.out.println("鼠标已连接,可以移动光标"); |
|||
} |
|||
|
|||
@Override |
|||
public void close() { |
|||
System.out.println("鼠标已断开"); |
|||
} |
|||
} |
|||
|
|||
// Keyboard类实现USB接口
|
|||
class Keyboard implements USB { |
|||
@Override |
|||
public void open() { |
|||
System.out.println("键盘已连接,可以输入文字"); |
|||
} |
|||
|
|||
@Override |
|||
public void close() { |
|||
System.out.println("键盘已断开"); |
|||
} |
|||
} |
|||
|
|||
// Computer类
|
|||
class Computer { |
|||
public void useUSB(USB device) { |
|||
System.out.println(">>> 正在接入设备..."); |
|||
device.open(); |
|||
System.out.println("<<< 正在拔出设备..."); |
|||
device.close(); |
|||
System.out.println(); |
|||
} |
|||
} |
|||
|
|||
// 主类(文件名必须是 USBTest.java)
|
|||
public class USBTest { |
|||
public static void main(String[] args) { |
|||
Computer computer = new Computer(); |
|||
|
|||
USB mouse = new Mouse(); |
|||
USB keyboard = new Keyboard(); |
|||
|
|||
computer.useUSB(mouse); |
|||
computer.useUSB(keyboard); |
|||
} |
|||
} |
|||
|
Before Width: | Height: | Size: 479 KiB |
@ -1,66 +0,0 @@ |
|||
// 抽象类 Animal
|
|||
abstract class Animal { |
|||
public abstract void makeSound(); |
|||
} |
|||
|
|||
// 接口 Swimmable
|
|||
interface Swimmable { |
|||
void swim(); |
|||
} |
|||
|
|||
// Dog 类继承 Animal 并实现 Swimmable
|
|||
class Dog extends Animal implements Swimmable { |
|||
@Override |
|||
public void makeSound() { |
|||
System.out.println("狗叫:汪汪汪!"); |
|||
} |
|||
|
|||
@Override |
|||
public void swim() { |
|||
System.out.println("狗会游泳,正在狗刨式前进..."); |
|||
} |
|||
} |
|||
|
|||
// Cat 类继承 Animal,不实现 Swimmable
|
|||
class Cat extends Animal { |
|||
@Override |
|||
public void makeSound() { |
|||
System.out.println("猫叫:喵喵喵~"); |
|||
} |
|||
} |
|||
|
|||
// 测试主类
|
|||
public class AnimalSystem { |
|||
public static void main(String[] args) { |
|||
// 多态调用:父类引用指向子类对象
|
|||
Animal dog = new Dog(); |
|||
Animal cat = new Cat(); |
|||
|
|||
System.out.println("===== 动物叫声系统 ====="); |
|||
dog.makeSound(); |
|||
cat.makeSound(); |
|||
|
|||
System.out.println("\n===== 游泳测试 ====="); |
|||
// Dog 实现了 Swimmable,可以调用 swim()
|
|||
if (dog instanceof Swimmable) { |
|||
((Swimmable) dog).swim(); |
|||
} |
|||
|
|||
// Cat 没有实现 Swimmable,不能调用 swim()
|
|||
if (cat instanceof Swimmable) { |
|||
((Swimmable) cat).swim(); |
|||
} else { |
|||
System.out.println("猫不会游泳,所以不能调用 swim() 方法"); |
|||
} |
|||
|
|||
System.out.println("\n===== 完整多态演示 ====="); |
|||
// 使用 Swimmable 接口类型引用 Dog 对象
|
|||
Swimmable swimmableDog = new Dog(); |
|||
swimmableDog.swim(); |
|||
|
|||
// 验证 Dog 和 Cat 的类型
|
|||
System.out.println("\ndog 是 Dog 的实例?" + (dog instanceof Dog)); |
|||
System.out.println("dog 是 Swimmable 的实例?" + (dog instanceof Swimmable)); |
|||
System.out.println("cat 是 Swimmable 的实例?" + (cat instanceof Swimmable)); |
|||
} |
|||
} |
|||