Java 挑战(三)
四、字符串
字符串是提供多种方法的字符序列。在这一章中,你将学习这个主题,并通过各种练习进行练习。
4.1 导言
对于处理字符串,有类String、StringBuffer,和StringBuilder。三者都满足接口java.lang.CharSequence。
接口字符序列
只读接口CharSequence主要提供对类型为char的单个字符以及类型为CharSequence的字符序列的索引访问。为此,特别声明了以下方法:
public interface CharSequence
{
public char charAt(int index);
public int length();
public CharSequence subSequence(int start, int end);
public String toString();
}
因此,接口CharSequence使得以索引方式处理字符串成为可能,而无需具体了解具体类型。这使得接口更加通用。
Attention: Things to know about the Interface Charsequence
接口CharSequence没有断言equals(Object)和hashCode()的行为。因此,equals(Object)比较两个CharSequence实例的结果是不确定的。例如,如果两个实例都是类型String,如果文本内容相同,结果将为真。然而,CharSequence实例也可以是StringBuffer类型。类型String和StringBuffer的比较总是产生false。但是,从 Java 11 开始,CharSequence.compare(CharSequence, CharSequence)对于在文本上比较两个 CharSequences 很有用。
类别字符串
类java.lang.String表示字符序列,在 Java 8 之前,这些字符序列由 Unicode 字符组成,并将它们的内容存储为数组char。从 Java 9 开始,内容被建模为byte[],并根据编码进行评估。字符串可以通过类string的构造函数调用来创建,也可以作为引用字符串来创建,如下面两行所示:
final String stringObject = new String("New String Object");
final String stringLiteral = "Stringliteral";
与实践相关的方法
以下方法对于练习和练习尤为重要:
-
length()获取字符串的长度。 -
isEmpty()检查字符串是否为空。 -
trim()删除文本开头和结尾的空白。 -
toUpperCase()/toLowerCase()创建一个仅由大写和小写字母组成的新字符串。 -
c
harAt(index)提供对单个字符的基于索引的访问。 -
toCharArray()根据字符串创建相应的char[]。 -
chars()基于字符串创建一个IntStream。之后,stream API 的丰富功能就可用了。 -
substring(startIndexIncl)将一个部分提取到一个新字符串中,该新字符串由从给定起始索引到原始字符串末尾的原始字母组成。 -
substring(startIndexIncl, endIndexExcl)从给定的起始索引到结束索引(不包括)提取由原始字母组成的新子串。 -
indexOf(String)检查提供的子字符串是否包含在主字符串中,并返回位置(如果没有找到匹配项,则返回-1)。
Java 11 提供了其他有趣的方法,例如,将一个字符串重复 n 次(repeat(n)),基于 Unicode 的空格测试(isBlank()),以及删除空格的方法(strip()、stripLeading()和stripTrailing())。
4.1.3 类 StringBuffer 和 StringBuilder
通常,在处理文本信息时,需要对现有的字符串进行修改。然而,对于String类的实例,由于它们的不变性,这只能通过诸如构造新对象的技巧来实现。或者,StringBuffer和StringBuilder类可以用于字符串操作。两者都有相同的 API,不同之处在于StringBuffer类的方法是同步的。对于StringBuilder类的方法来说,情况并非如此,只要只有一个线程在处理它,这并不重要。
Attention: String Concatenations and Performance
一个常见的技巧是在准备大文本输出时使用StringBuffer和StringBuilder而不是+运算符。由于 Java 编译器和 JVM 都自动执行各种优化,您应该更喜欢使用+运算符的 1 简单字符串连接的可读性。例如,
String example = "text start" + "XXX" + "text end";
比像这样的结构更容易阅读
final String example = new StringBuilder().append("text start").
append("XXX").
append("text end").toString();
因此,只有对于真正性能关键的部分,建议使用StringBuilder对象实现字符串连接。对于这本书,我更喜欢可读性和可理解性,因此很少使用StringBuilder,只有当String中没有提供的特殊功能令人感兴趣时。
附加功能和与类字符串的比较
方便的是,StringBuffer和StringBuilder类都有删除操作。通过方法deleteCharAt()和delete(),可以从字符串表示中删除字符。一个名为insert()的类似方法允许你插入字符。
不过StringBuilder和StringBuffer都有一定的缺点。他们缺乏各种方法,如toLowerCase()和toUpperCase()。相反,它们提供了(不太常用的)reverse()方法,以相反的顺序返回内容。表 4-1 显示了String类的一些重要方法以及到StringBuffer或StringBuilder的映射。
表 4-1
重要字符串方法的映射
|字符串 1
|
字符串缓冲区/字符串生成器
|
| --- | --- |
| +, +=, concat() | append() |
| replace(), subString() | replace(), subString() |
| indexOf(), startsWith() | indexOf() |
| endsWith() | lastIndexOf() |
| -没有可用方法!—— | reverse() |
| -没有可用方法!—— | insert() |
| -没有可用方法!—— | delete(), deleteCharAt() |
| toUpperCase()/toLowerCase() | -没有可用方法!—— |
4.1.4 阶级性
Character类抽象出单个字符,并可以通过各种助手方法提供某些信息,比如它是字母还是空格:
-
isLetter()检查字符是否代表字母。 -
isDigit()检查字符是否对应十进制数字。 -
isUpperCase()/isLowerCase()检查字符是代表大写字母还是小写字母。 -
将给定字符转换成大写或小写字母。
-
isWhiteSpace()检查字符是否被解释为空白(即空格、制表符等)。). -
getNumericValue()获取数值。这对于数字非常方便,对于十六进制数字也是如此。
例子
让我们看一个小例子:
System.out.println(Character.getNumericValue('0'));
System.out.println(Character.getNumericValue('7'));
System.out.println(Character.getNumericValue('9'));
System.out.println(Character.getNumericValue('A'));
System.out.println(Character.getNumericValue('a'));
System.out.println(Character.getNumericValue('F'));
System.out.println(Character.getNumericValue('f'));
System.out.println(Character.getNumericValue('Z'));
System.out.println(Character.getNumericValue('z'));
上面的行提供了以下输出:
0
7
9
10
10
15
15
35
35
正如您很容易看到的,如果您必须在不同的数字系统之间执行转换,或者如果您想要获得一个字母的数值,那么getNumericValue()方法非常有用。
4.1.5 与字符和字符串相关的示例
为了结束介绍,让我们看两个使用Character和String classes的例子。
自制字符转换
使用Character类,您可以执行从文本数字到数字值的转换。对于十进制数字,以下转换是常见的。类似的东西可以用于字母表中的字母。
int digitValue = digitAsChar - '0';
int posOfChar = currentChar - 'A';
对于十六进制数,则需要进一步区分。下面以十六进制数转换为十进制数为例来说明getNumericValue()的优势。下面我展示了哪些步骤是必要的——首先使用方法getNumericValue(),其次使用自定义创建hexDigitToDecimal()。请注意,自定义变量不支持小写十六进制数!
static int convertToDecimal(final String hexDigits)
{
int valueOldStyle = 0;
int valueNewStyle = 0;
for (int i = 0; i < hexDigits.length(); i++)
{
final char currentChar = hexDigits.charAt(i);
// OLD and cumbersome: invoking own method
int digitValueOld = hexDigitToDecimal(currentChar);
valueOldStyle = valueOldStyle * 16 + digitValueOld;
// NEW and short and crisp: JDK Method
int digitValue = Character.getNumericValue(currentChar);
valueNewStyle = valueNewStyle * 16 + digitValue;
}
return valueNewStyle;
}
// OLD and cumbersome: Implementation of own method
static int hexDigitToDecimal(final char currentChar)
{
if (Character.isDigit(currentChar))
return currentChar - '0';
// "Optimistische" Annahme: A ... F
return currentChar - 'A' + 10;
}
自制的转换是脆弱的,尤其依赖于正确的字符——所以理想情况下,您应该事先进行有效性检查。
可能的可读选择要将一个十六进制数的字符转换成十进制值,可以使用indexOf()来确定在预定义值集中的位置。然而,前面提到的小写字母作为数字的弱点仍然存在。
static int hexDigitToDecimalAlternative(final char hexDigit)
{
final int position = "0123456789ABCDEF".indexOf(hexDigit);
if (position < 0)
throw new IllegalArgumentException("invalid char: " + hexDigit);
return position;
}
其他特点很多时候,我们只会想到普通的汉字,或许还有元音字母。因此,人们可以直观地假设isDigit()只检查数字 0 到 9 的 ASCII 字符。但事实并非如此!还有其他(更有趣的)数字可以正确转换。这里getNumericValue()的优势是显而易见的:
System.out.println("\u0669");
System.out.println(Character.isDigit('\u0669'));
System.out.println(Character.getNumericValue('\u0669'));
System.out.println(hexDigitToDecimal('\u0669'));
这导致了图 4-1 所示的输出。
图 4-1
数字的特殊表示
示例:字符串处理
以类String为例,您想要计算每个字母出现的次数,同等对待小写和大写字母。对于文本“Otto ”,通过将其转换为小写字母,您会得到 2 x t 和 2 x o。这种处理也被称为直方图。直方图是对象分布的表示,通常是数值。从摄影中已知图像的亮度分布。在下文中,它涉及文本的字母频率的分布和/或确定:
static Map<Character, Integer> generateCharacterHistogram(final String word)
{
final Map<Character, Integer> charCountMap = new TreeMap<>();
final char[] chars = word.toLowerCase().toCharArray();
for (char currentChar : chars)
{
if (Character.isLetter(currentChar))
{
// Trick, but attention to the order!
charCountMap.putIfAbsent(currentChar, 0);
charCountMap.computeIfPresent(currentChar,
(key, value) -> value + 1);
// Alternative
// final int count = charCountMap.getOrDefault(currentChar, 0);
// charCountMap.put(currentChar, count + 1);
}
}
return charCountMap;
}
让我们在 JShell 中尝试一下:
jshell> generateCharacterHistogram("Otto")
$9 ==> {o=2, t=2}
jshell> generateCharacterHistogram("Hello Michael")
$10 ==> {a=1, c=1, e=2, h=2, i=1, l=3, m=1, o=1}
jshell> generateCharacterHistogram("Java Challenge, Your Java-Training")
$11 ==> {a=6, c=1, e=2, g=2, h=1, i=2, j=2, l=2, n=3, o=1, r=2, t=1, u=1, v=2, y=1}
Note: Assistance in Java 8
顺便说一下,在 Java 8 中,几个有用的方法被添加到了Map<K,V>接口中,其中包括:
-
putIfAbsent() -
computeIfPresent()
它们通常允许更容易地编写算法。请记住,调用的顺序很重要。
4.2 练习
4.2.1 练习 1:数字转换(★★✩✩✩)
基于一个字符串,实现二进制数的验证、转换以及十六进制数的验证。
Note
转换可以用Integer.parseInt(value, radix)解决,二进制数以 2 为基数,十六进制数以 16 为基数。不要显式使用这些,而是自己实现。
例子
|投入
|
方法
|
结果
| | --- | --- | --- | | “10101” | isBinaryNumber() | 真实的 | | “111” | binarutodecimal_) | seven | | “AB” | 十六进制() | One hundred and seventy-one |
练习 1a (★✩✩✩✩)
编写方法boolean isBinaryNumber(String),检查给定的字符串是否只由字符 0 和 1 组成(即表示一个二进制数)。
练习 1b (★★✩✩✩)
编写方法int binaryToDecimal(String),将表示为字符串的(有效)二进制数转换为相应的十进制数。
练习 1c (★★✩✩✩)
再次编写整个转换,但这次是十六进制数。
4.2.2 练习 2:加入者(★✩✩✩✩)
练习 2a (★✩✩✩✩)
编写方法String join(List<String>, String),用指定的分隔符字符串连接字符串列表,并将其作为一个字符串返回。最初自己实现,不使用任何特殊的 JDK 功能。
练习 2b (★✩✩✩✩)
在方法String joinStrings(List<String>, String)中使用流 API 中的适当方法实现字符串连接。
例子
|投入
|
分离器
|
结果
| | --- | --- | --- | | [“你好”、“世界”、“消息”] | " +++ " | " hello ++ world ++ message " | | [“米迦”、“苏黎世”] | “喜欢” | “米莎喜欢苏黎世” |
4.2.3 练习 3:反串(★★✩✩✩)
Write 方法String reverse(String),它反转字符串中的字母并返回结果。自己实现它,不要使用任何特殊的 JDK 功能,比如来自StringBuilder类的reverse()方法。
例子
|投入
|
结果
| | --- | --- | | “ABCD” | " DCBA " | | “奥托” | “奥托” | | “彼得 | " RETEP " |
4.2.4 练习 4:回文(★★★✩✩)
练习 4a (★★✩✩✩)
编写方法boolean isPalindrome(String),不管大小写,检查给定的字符串是否是回文。回文是一个从正面和背面读起来都一样的单词。
Note
用StringBuilder.reverse()就可以轻松解决验证。明确地不要使用 JDK 组件,而是自己实现功能。
例子
|投入
|
结果
| | --- | --- | | “奥托” | 真实的 | | " ABCBX " | 错误的 | | " ABCXcba " | 真实的 |
练习 4b (★★★✩✩)
编写一个扩展,也不考虑空格和标点符号的相关性,允许检查整个句子,如下所示:
Was it a car or a cat I saw?
4.2.5 练习 5:无重复字符(★★★✩✩)
确定给定的字符串是否不包含重复的字母。大写和小写字母不应该有任何区别。为此编写方法boolean checkNoDuplicateChars(String)。
例子
|投入
|
结果
| | --- | --- | | “奥托” | 错误的 | | "阿德里安" | 错误的 | | “米莎” | 真实的 | | 《ABCDEFG》 | 真实的 |
4.2.6 练习 6:删除重复的字母(★★★✩✩)
Write 方法String removeDuplicates(String),在给定的文本中每个字母只保留一次,因此删除所有后续的重复字母,而不考虑大小写。但是,应该保留字母的原始顺序。
例子
|投入
|
结果
| | --- | --- | | “香蕉” | “禁令” | | “拉拉妈妈” | “林” | | “迈克尔” | “迈克尔” |
4.2.7 练习 7:资本化(★★✩✩✩)
练习 7a (★★✩✩✩)
Write 方法String capitalize(String)将给定文本转换成英文标题格式,其中每个单词都以大写字母开头。
例子
|投入
|
结果
| | --- | --- | | “这是一个非常特别的标题” | “这是一个非常特别的标题” | | “有效的 java 很棒” | “有效的 Java 很棒” |
练习 7b:修改(★★✩✩✩)
现在假设输入是一个字符串列表,应该返回一个字符串列表,每个单词以大写字母开始。使用以下签名作为起点:
List<String> capitalize(List<String> words)
练习 7c:特殊待遇(★★✩✩✩)
在标题中,经常会遇到像“is”或“a”这样的非大写单词的特殊处理。将它实现为方法List<String> capitalizeSpecial(List<String>, List<String>,该方法获取要从转换中排除的单词作为第二个参数。
例子
|投入
|
例外
|
结果
| | --- | --- | --- | | 【“这个”、“是”、“一个”、“标题”】 | [“是”,“一个”] | 【“这个”、“是”、“一个”、“标题”】 |
4.2.8 练习 8:轮换(★★✩✩✩)
考虑两个字符串,str1和str2,其中第一个字符串应该比第二个长。弄清楚第一个是否包含另一个。这样做时,第一个字符串中的字符也可以被旋转。字符可以从开头或结尾移动到相反的位置(甚至重复)。为此,创建方法boolean containsRotation(String, String),它在检查过程中不区分大小写。
例子
|输入 1
|
输入 2
|
结果
| | --- | --- | --- | | “ABCD” | “ABC” | 真实的 | | “ABCDEF | 《EFAB》 | 真(“abcdef”↢x 2“CD EFAB”包含“efab”) | | 《BCDE》 | "欧共体" | 错误的 | | “挑战” | "壁虎" | 真实的 |
4.2.9 练习 9:格式良好的大括号(★★✩✩✩)
编写方法boolean checkBraces(String),它检查作为字符串传递的圆括号序列是否包含匹配的(正确嵌套的)括号对。
例子
|投入
|
结果
|
评论
| | --- | --- | --- | | "(())" | 真实的 | | | "()()" | 真实的 | | | "(()))((())" | 错误的 | 虽然相同数量的左大括号和右大括号,但它们没有正确嵌套 | | "((()" | 错误的 | 没有合适的支撑 |
4.2.10 练习 10:字谜(★★✩✩✩)
术语变位词用于描述两个包含相同频率的相同字母的字符串。在这里,大写和小写不应该有任何区别。写方法boolean isAnagram(String, String)。
例子
|输入 1
|
输入 2
|
结果
| | --- | --- | --- | | “奥托” | “托托” | 真实的 | | “玛丽 | “军队” | 真实的 | | “阿纳纳斯” | “香蕉” | 错误的 |
4.2.11 练习 11:莫尔斯电码(★★✩✩✩)
能够将给定文本翻译成莫尔斯电码字符的书写方法String toMorseCode(String)。它们由每个字母一至四个长短音调的序列组成,用句号(.)或连字符(-)。为了更容易区分,希望在每个音调之间放置一个空格,在每个字母音调序列之间放置三个空格。否则,S(...)和 EEE(...)将无法彼此区分。
为简单起见,将自己限制为字母 E、O、S、T 和 W,编码如下:
|信
|
莫尔斯电码
| | --- | --- | | E | 。 | | O | - - - | | S | ... | | T | - | | W | 。- - |
例子
|投入
|
结果
| | --- | --- | | 无线电紧急呼救信号 | ...- - - ... | | 自录音再现装置发出的高音 | - .- - .。- | | 西方的 | 。- - ....- |
奖励试着找出字母表中所有字母对应的莫尔斯电码,(比如转换你的名字)。你可以在 https://en.wikipedia.org/wiki/Morse_code 找到必要的提示。
4.2.12 练习 12:模式检查器(★★★✩✩)
编写方法boolean matchesPattern(String, String),该方法根据以单个字符的形式作为第一个参数传递的模式的结构来检查空格分隔的字符串(第二个参数)。
例子
|输入模式
|
输入文本
|
结果
| | --- | --- | --- | | " xyyx " | 《蒂姆·迈克·迈克·蒂姆》 | 真实的 | | " xyyx " | 《蒂姆·迈克·汤姆·蒂姆》 | 错误的 | | " xyxx " | 《蒂姆·迈克·迈克·蒂姆》 | 错误的 | | " xxxx " | "团队团队团队" | 真实的 |
4.2.13 练习 13:网球比分(★★★✩✩)
编写方法String tennisScore(String, String, String),根据两个玩家 PL1 和 PL2 的文本分数,以熟悉的风格发布公告,如十五爱、二或优势玩家 X 。因此,他们的分数以格式< PL1 分> : < PL2 分>给出。
以下计数规则适用于网球比赛:
-
当玩家达到 4 分或更多,并且领先至少 2 分时,游戏获胜(游戏)。
-
从 0 到 3 的分数被命名为爱,十五,三十和四十。
-
在至少 3 分和平局的情况下,这被称为平手。
-
至少有 3 分和 1 分的差距,对于多一分的人来说,这叫优势。
例子
|投入
|
得分
| | --- | --- | | 1:0 米夏蒂姆 | 《十五个爱》 | | 2:2 米夏蒂姆 | “三点半” | | 2:3 米夏蒂姆 | “三点四十” | | 3:3 米夏蒂姆 | “平手” | | 4:3 米夏蒂姆 | “优势米查” | | 4:4 米夏蒂姆 | “平手” | | 5:4 米夏蒂姆 | “优势米查” | | 6:4 米夏蒂姆 | 《游戏米莎》 |
4.2.14 练习 14:版本号(★★✩✩✩)
编写方法int compareVersions(String, String),允许你以主的格式比较版本号。小调。互相贴片——因此贴片的规格是可选的。特别是,返回值应该与来自Comparator<T>接口的int compare(T, T)方法兼容。
例子
|版本 1
|
版本 2
|
结果
| | --- | --- | --- | | 1.11.17 | 2.3.5 | < | | Two point one | 2.1.3 | < | | 2.3.5 | Two point four | < | | Three point one | Two point four | > | | Three point three | 3.2.9 | > | | 7.2.71 | 7.2.71 | = |
奖励使用接口Comparator<T>实现功能。
4.2.15 练习 15:转换 strToLong (★★✩✩✩)
将一个字符串转换成一个long。自己写方法long strToLong(String)。
Note
使用long.parseLong(value)可以轻松实现转换。不要显式使用它,而是自己实现整个转换。
例子
|投入
|
结果
|
| --- | --- |
| “+123” | One hundred and twenty-three |
| “-123” | -123 |
| “7271” | Seven thousand two hundred and seventy-one |
| “ABC” | IllegalArgumentException |
| “0123” | 83(对于奖励任务) |
| “-0123” | -83(对于奖励任务) |
| “0128” | IllegalArgumentException(奖励任务) |
奖励启用八进制数解析。
4.2.16 练习 16:打印塔(★★★✩✩)
编写方法void printTower(int),将堆叠在一起的 n 片的塔表示为 ASCII 图形,用字符#表示,并绘制一条下边界线。
示例高度为 3 的塔应该是这样的:
|
# | #
## | ##
### | ###
---------
4.3 解决方案
4.3.1 解决方案 1:数字转换(★★✩✩✩)
基于一个字符串,实现二进制数的验证、转换以及十六进制数的验证。
Note
转换可以用Integer.parseInt(value, radix)解决,二进制数以 2 为基数,十六进制数以 16 为基数。不要显式地使用它们,而是自己实现它们。
例子
|投入
|
方法
|
结果
| | --- | --- | --- | | “10101” | isBinaryNumber() | 真实的 | | “111” | binarutodecimal_) | seven | | “AB” | 十六进制() | One hundred and seventy-one |
解决方案 1a (★✩✩✩✩)
编写方法boolean isBinaryNumber(String),检查给定的字符串是否只由字符 0 和 1 组成(即表示一个二进制数)。
算法Java 中的实现从头到尾一个字符一个字符的遍历字符串,检查当前字符是 0 还是 1。如果检测到另一个字符,循环终止,然后返回false:
public static boolean isBinaryNumber(final String number)
{
boolean isBinary = true;
int i = 0;
while (i < number.length() && isBinary)
{
final char currentChar = number.charAt(i);
isBinary = (currentChar == '0' || currentChar == '1');
i++;
}
return isBinary;
}
备选案文 1b
编写方法int binaryToDecimal(String),将表示为字符串的(有效)二进制数转换为相应的十进制数。
算法从左到右逐个字符地遍历字符串,并将每个字符作为二进制数字处理。当前字符用于通过将先前转换的值乘以 2 并加上当前值来计算值。后者由减法number.charAt(i) - '0'决定,正如你在十进制数的介绍部分所学的。可以更清楚地制定算法,这意味着无需特殊处理,因为有效输入由之前实现的方法isBinaryNumber()确保。
public static int binaryToDecimal(final String number)
{
if (!isBinaryNumber(number))
throw new IllegalArgumentException(number + " is not a binary number");
int decimalValue = 0;
for (int i = 0; i < number.length(); i++)
{
final int current = number.charAt(i) - '0';
decimalValue = decimalValue * 2 + current;
}
return decimalValue;
}
解决方案 1c
再次编写整个转换,但这次是十六进制数。
算法对于十六进制数,因子必须改为 16。另外,getNumericValue()适用于确定值。
public static int hexToDecimal(final String number)
{
if (!isHexNumber(number))
throw new IllegalArgumentException(number + " is not a hex number");
int decimalValue = 0;
for (int i = 0; i < number.length(); i++)
{
final char currentChar = number.charAt(i);
final int value = Character.getNumericValue(currentChar);
decimalValue = decimalValue * 16 + value;
}
return decimalValue;
}
有效十六进制数的检查使用十进制数介绍部分介绍的isDigit()方法,并手动检查从 A 到 F 的字母:
public static boolean isHexNumber(final String number)
{
boolean isHex = true;
final String upperCaseNumber = number.toUpperCase();
int i = 0;
while (i < upperCaseNumber.length() && isHex)
{
final char currentChar = upperCaseNumber.charAt(i);
isHex = Character.isDigit(currentChar) ||
currentChar >= 'A' && currentChar <= 'F';
i++;
}
return isHex;
}
这个挑战是一个搜索问题。搜索字符串中第一个出现的字母,对于二进制数,该字母不是 0 或 1,对于十六进制数,该字母不在 0 到 F 的范围内。像这样的搜索问题也可以使用一个while循环来解决——那么i >= length() || !condition就适用了。
int i = 0;
while (i < input.length() && condition)
{
// teste Bedingung
i++;
}
Hint: Possible Alternatives and Optimizations
尽管所示的实现相当简单,但有一些非常优雅的替代方法也很容易阅读和理解,即检查字母 A 到 F 的序列,以查看是否包含该字符:
isHex = Character.isDigit(currentChar) || "ABEDEF".contains(currentChar);
实际上,基于最后一个想法,完整的过程可以被缩短:
isHex = "0123456789ABCDEF".indexOf(currentChar) >= 0;
或者,通过使用正则表达式,甚至可以使整个检查大大缩短:
static boolean isHexNumber(final String number)
{
return number.matches("^[0-9a-fA-F]+$");
}
确认
测试时,使用以下显示正确操作的输入:
@ParameterizedTest(name = "isBinaryNumber({0}) => {1}")
@CsvSource({ "10101, true", "222, false", "12345, false" })
public void isBinaryNumber(String value, boolean expected)
{
boolean result = Ex01_BasicNumberChecks.isBinaryNumber(value);
assertEquals(expected, result);
}
@ParameterizedTest(name = "binaryToDecimal({0}) => {1}")
@CsvSource({ "111, 7", "1010, 10", "1111, 15", "10000, 16" })
public void binaryToDecimal(String value, int expected)
{
int result = Ex01_BasicNumberChecks.binaryToDecimal(value);
assertEquals(expected, result);
}
@ParameterizedTest(name = "hexToDecimal({0}) => {1}")
@CsvSource({ "7, 7", "A, 10", "F, 15", "10, 16" })
public void hexToDecimal(String value, int expected)
{
int result = Ex01_BasicNumberChecks.hexToDecimal(value);
assertEquals(expected, result);
}
4.3.2 解决方案 2:加入者(★✩✩✩✩)
解决方案 2a (★✩✩✩✩)
编写方法String join(List<String>, String),用指定的分隔符字符串连接字符串列表,并将其作为一个字符串返回。最初自己实现,不使用任何特殊的 JDK 功能。
例子
|投入
|
分离器
|
结果
| | --- | --- | --- | | [“你好”、“世界”、“消息”] | " +++ " | " hello ++ world ++ message " | | [“米迦”、“苏黎世”] | “喜欢” | “米莎喜欢苏黎世” |
算法从前到后遍历值列表。在每种情况下,将文本插入到StringBuilder中,添加分隔符字符串,并重复此操作,直到最后一个值。作为特殊处理,不要在最后一个字符串后添加分隔符。
static String join(final List<String> values, final String delimiter)
{
var sb = new StringBuilder();
for (int i = 0; i < values.size(); i++)
{
sb.append(values.get(i));
// No separator after last occurrence
if (i < values.size() - 1)
{
sb.append(delimiter);
}
}
return sb.toString();
}
解决方案 2b (★✩✩✩✩)
在方法String joinStrings(List<String>, String)中使用流 API 中的适当方法实现字符串连接。
算法通过使用流 API,可以用简洁易懂的方式很好地表达挑战,无需任何特殊处理,如下所示:
static String joinStrings(final List<String> values, final String delimiter)
{
return values.stream().collect(Collectors.joining(delimiter));
}
确认
测试时,使用以下显示正确操作的输入:
@Test
public void testJoinLowLevel()
{
var result = Ex02_StringJoiner.join(List.of("hello", "world", "message")," +++ ");
assertEquals("hello +++ world +++ message", result);
}
@Test
public void testJoinStringsWithStream()
{
var result = Ex02_StringJoiner.joinStrings(List.of("Micha", "Zurich")," likes ");
assertEquals("Micha likes Zurich", result);
}
4.3.3 解决方案 3:反串(★★✩✩✩)
Write 方法String reverse(String),它反转字符串中的字母并返回结果。自己实现它,不要使用任何特殊的 JDK 功能,比如来自StringBuilder类的reverse()方法。
例子
|投入
|
结果
| | --- | --- | | “ABCD” | " DCBA " | | “奥托” | “奥托” | | “彼得 | " RETEP " |
算法最初,一个想法可以是从末尾开始逐个字符地遍历原始字符串,并将相应的字符添加到结果中:
static String reverse(final String original)
{
String reversed = "";
for (int i = original.length() - 1; i >= 0; i--)
{
char currentChar = original.charAt(i);
reversed += currentChar;
}
return reversed;
}
然而,存在一个小问题:用+=连接字符串可能开销很大,因为这样会创建新的字符串对象。出于这个原因,对于更复杂的操作,使用一个StringBuilder的实例可能会更好。我将在解决方案的第二部分讨论进一步的可能性。
优化算法简单地问自己:例如,如果非常长的字符串需要非常频繁地反转,如何才能更有效地利用内存?
想法是使用toCharArray()将字符串转换为char[],并直接在char[]上工作。此外,还使用了两个名为 left 和 right 的位置指针,最初指向第一个和最后一个字符。现在你交换相应的字母,位置指针向内移动。只要左右<有效,重复整个过程;如果左>* = 右该过程被中止。下面举例说明文本ABCD的流程,其中l代表左和r代表右:*
A B C D
l r
D B C A
l r
D C B A
r l => end
您可以按如下方式实现所描述的过程:
static String reverseInplace(final String original)
{
final char[] originalChars = original.toCharArray();
int left = 0;
int right = originalChars.length - 1;
while (left < right)
{
final char leftChar = originalChars[left];
final char rightChar = originalChars[right];
// swap
originalChars[left] = rightChar;
originalChars[right] = leftChar;
left++;
right--;
}
return String.valueOf(originalChars);
}
确认
让我们编写一个单元测试来验证所需的功能:
@ParameterizedTest(name = "reverse({0}) => {1}")
@CsvSource({ "ABCD, DCBA", "OTTO, OTTO", "PETER, RETEP" })
void testReverse(final String input, final String expectedOutput)
{
final String result = Ex03_ReverseString.reverse(input);
assertEquals(expectedOutput, result);
}
您可以对 inplace 版本做类似的事情,但是这里您只使用了 JShell 中的两个调用:
jshell> reverseInplace("ABCD")
$29 ==> "DCBA"
jshell> reverseInplace("PETER")
$30 ==> "RETEP"
4.3.4 解决办法 4:回文
解决方案 4a (★★✩✩✩)
编写方法boolean isPalindrome(String),不管大小写,检查给定的字符串是否是回文。回文是一个从正面和背面读起来都一样的单词。
Note
用StringBuilder.reverse()就可以轻松解决验证。明确地不要使用 JDK 组件,而是自己实现功能。
例子
|投入
|
结果
| | --- | --- | | “奥托” | 真实的 | | " ABCBX " | 错误的 | | " ABCXcba " | 真实的 |
Job Interview Tips
作为求职面试的一个例子,我再次列出了你可能会问的问题,以澄清任务的范围:
-
应该区分大小写吗?回答:没有,任何
-
空格相关吗?回答:先是有,后来没有,然后被忽略
算法如练习 3 反串,字符串表示为char[],你从左向内推进一个位置,从右向内推进一个位置,只要字符匹配,只要左位置仍然小于右位置:
static boolean isPalindrome(final String input)
{
final char[] chars = input.toLowerCase().toCharArray();
int left = 0;
int right = chars.length-1;
boolean isSameChar = true;
while (left < right && isSameChar)
{
isSameChar = (chars[left] == chars[right]);
left++;
right--;
}
return isSameChar;
}
带递归的算法:不使用char[]作为辅助数据结构,如何递归求解回文问题?在阅读了第三章并解决了那里给出的一些递归练习题后,你应该能够很容易地实现它。考虑到 helper 方法的策略或习惯用法,下面的递归实现出现了,它从外部开始,总是检查两个字符。只要字符匹配,这就向内继续,并且左边的位置比右边的位置小。
public static boolean isPalindromeRec(final String input)
{
return isPalindromeRec(input.toLowerCase(), 0, input.length() - 1);
}
static boolean isPalindromeRec(final String input,
final int left, final int right)
{
if (left >= right)
return true;
if (input.charAt(left) == input.charAt(right))
{
return isPalindromeRec(input, left + 1, right - 1);
}
return false;
}
另一种方法是总是用字符来缩短字符串。为什么这个逻辑上的解决方案实际上不那么好?答案是显而易见的:这会导致创建许多临时字符串对象。此外,必须进行大量的复制操作。
备选案文 4b
编写一个扩展,也不考虑空格和标点符号的相关性,允许检查整个句子,如下所示:
Was it a car or a cat I saw?
算法你可以在算法中加入特殊的空格检查。尽管如此,创建一个方法版本并在调用原始方法之前预先替换掉所有不需要的标点和空格还是比较容易的:
public static boolean isPalindrome(final String input,
final boolean ignoreSpacesAndPunctuation)
{
String adjustedInput = input.toLowerCase();
if (ignoreSpacesAndPunctuation)
adjustedInput = input.replaceAll(" |!|\\.", "");
return isPalindromeRec(adjustedInput);
}
请注意,replaceAll()中使用了正则表达式来从要检查的文本中删除字符,即空格、感叹号和句点。点必须特别屏蔽,因为它代表正则表达式中的任何字符。
确认
为了进行验证,您再次使用以下显示正确操作的输入编写单元测试:
@ParameterizedTest(name = "isPalindromeRec({0} => {1}")
@CsvSource({ "Otto, true",
"ABCBX, false",
"ABCXcba, true" })
void isPalindromeRec(String value, boolean expected)
{
boolean result = Ex04_Palindrome.isPalindromeRec(value);
assertEquals(expected, result);
}
@ParameterizedTest(name = "''{0}'' should be {1}")
@CsvSource( { "Dreh mal am Herd., true",
"Das ist kein Palindrom!, false"} )
void isPalindrome(String value, boolean expected)
{
boolean result = Ex04_Palindrome.isPalindrome(value, true);
assertEquals(expected, result);
}
Findings: Pay Attention to Comprehensibility
由于其 API 和基于位置/索引的访问,字符串选择迭代解决方案是绝对自然的。如果必须确定数字的回文属性,这将不再方便。这可以通过递归和一些考虑来完成,即使没有 3.3.10 节中练习 10 的解决方案所示的通过转换的迂回方法。在上一个练习中开发了功能reverse(),您可以按如下方式使用它:
static boolean isPalindrome(final String input)
{
final String upperInput = input.toUpperCase();
return upperInput.equals(reverse(upperInput));
}
这证明了问题和上下文感知编程能够创建可理解和可维护的解决方案。可理解的、可维护的和可改变的属性在实践中非常重要,因为由于变化的或新的需求,源代码通常比完全从零开始创建更频繁地被修改。
4.3.5 解决办法 5:无重复气体
确定给定的字符串是否不包含重复的字母。大写和小写字母不应该有任何区别。为此编写方法boolean checkNoDuplicateChars(String)。
例子
|投入
|
结果
| | --- | --- | | “奥托” | 错误的 | | "阿德里安" | 错误的 | | “米莎” | 真实的 | | 《ABCDEFG》 | 真实的 |
算法在解决这个问题时,你可能会想到将单个字符存储在Set<E>中。从前到后一次遍历输入的一个字符。对于每个字符,检查它是否已经存在于Set<E>中。如果是这样,您遇到了重复的字符并中止处理。否则,您将字符插入到Set<E>中,并继续处理下一个字符。
static boolean checkNoDuplicateChars(final String input)
{
final char[] allCharsOfInput = input.toLowerCase().toCharArray();
final Set<Character> containedChars = new HashSet<>();
for (char currentChar : allCharsOfInput)
{
if (containedChars.contains(currentChar))
return false;
containedChars.add(currentChar);
}
return true;
}
Hint: Possible Alternatives and Optimizations
尽管所示的实现非常清楚,但是通过利用任何字符串都可以使用chars()方法转换为IntStream这一事实,可以找到其他更紧凑的替代方法。试图保持逻辑与实际算法结果相当接近可能如下——其中您需要boxed()将int的值转换成Integer。这是将值插入Set<Integer>并以这种方式删除重复值的唯一方法。如果不存在重复项,Set<Integer>的计数必须等于字符串的长度。
static boolean checkNoDuplicateCharsStreamV1(final String input)
{
return input.toLowerCase().chars().
boxed().
collect(Collectors.toSet()).
size() == input.length();
}
这是更紧凑,但可能不太容易理解的经典版本。但是有一个合适的替代方法:您可以使用distinct()删除所有重复的元素,使用count()获得流中元素的数量。如果不存在重复项,计数必须等于字符串的长度。话多,指令少...整个事情可以用如下一行程序来表达:
boolean checkNoDuplicateCharsWithStreamOpt(final String input)
{
return input.toLowerCase().chars().distinct().count() == input.length();
}
确认
您再次使用单元测试来验证所需的功能:
@ParameterizedTest(name = "checkNoDuplicateChars({0}) => {1}")
@CsvSource({ "Otto, false", "Adrian, false", "Micha, true", "ABCDEFG, true" })
void checkNoDuplicateChars(final String input, final boolean expected)
{
var result = Ex05_CheckNoDuplicateChars.checkNoDuplicateChars(input);
assertEquals(expected, result);
}
4.3.6 解决方案 6:删除重复的信件(★★★✩✩)
Write 方法String removeDuplicates(String),在给定的文本中每个字母只保留一次,因此删除所有后续的重复字母,而不考虑大小写。但是,应该保留字母的原始顺序。
例子
|投入
|
结果
| | --- | --- | | “香蕉” | “禁令” | | “拉拉妈妈” | “林” | | “迈克尔 | “迈克尔” |
算法再次,您逐个字符地遍历字符串,并将相应的字符存储在名为alreadySeen的Set<E>中。如果当前字符尚未包含在内,它将被添加到Set<E>和结果文本中。但是,如果这样一个字符已经存在,您将继续输入下一个字符。
static String removeDuplicates(final String input)
{
var result = new StringBuilder();
var alreadySeen = new HashSet<>();
for (int i = 0; i < input.length(); i++)
{
final char currentChar = input.charAt(i);
if (!alreadySeen.contains(currentChar))
{
alreadySeen.add(currentChar);
result.append(currentChar);
}
}
return result.toString();
}
优化算法使用 Java 8 板载工具可以更优雅地解决整个练习。使用chars()方法将字符串转换成IntStream的可能性让您受益匪浅。stream API 允许您简单地通过使用distinct()来删除重复项。之后,将数值转换回char类型,最后转换成简短的单字符字符串。反过来,这些通过joining()组合成一个结果。
static String removeDuplicatesImproved(final String input)
{
return input.chars().distinct().
mapToObj(i -> (char) i + "").
collect(Collectors.joining());
}
确认
您可以使用以下单元测试来检查重复字母的删除情况:
@ParameterizedTest(name = "removeDuplicates({0}) => {1}")
@CsvSource({ "bananas, bans", "lalalamama, lam", "MICHAEL, MICHAEL" })
void testRemoveDuplicates(final String input, final String expected)
{
var result = Ex06_DuplicateCharsRemoval.removeDuplicates(input);
assertEquals(expected, result);
}
对于优化版本,这是以相同的方式完成的。
4.3.7 解决方案 7:资本化(★★✩✩✩)
溶液 7a (★★✩✩✩)
Write 方法String capitalize(String)将给定文本转换成英文标题格式,其中每个单词都以大写字母开头。
例子
|投入
|
结果
| | --- | --- | | “这是一个非常特别的标题” | “这是一个非常特别的标题” | | “有效的 java 很棒” | “有效的 Java 很棒” |
算法因为字符串是不可变的,所以最初你将内容复制到一个char[]中,在此基础上进行修改。你从前到后遍历这个数组,寻找一个新单词的开头。作为一个指标,你可以使用一面boolean旗capitalizeNextChar。这表示下一个单词的第一个字母必须大写。最初,这个标志是true,所以当前(第一个)字符被转换成大写字母。这只会发生在字母上,不会发生在数字上。转换后,标志被重置,字母被跳过,直到找到一个空格。然后,您将标志重置为true。重复这个过程,直到到达数组的末尾。最后,从包含修改的数组中创建一个新的字符串。
static String capitalize(final String input)
{
final char[] inputChars = input.toCharArray();
boolean capitalizeNextChar = true;
for (int i = 0; i < inputChars.length; i++)
{
var currentChar = inputChars[i];
if (Character.isWhitespace(currentChar))
{
capitalizeNextChar = true;
}
else
{
if (capitalizeNextChar && Character.isLetter(currentChar))
{
inputChars[i] = Character.toUpperCase(currentChar);
capitalizeNextChar = false;
}
}
}
return new String(inputChars);
}
让我们在 JShell 中尝试整个事情:
jshell> capitalize("everything seems fine")
$14 ==> "Everything Seems Fine"
但是,现在您可能想知道数字后面的字母或其他非字母应该出现的行为:
jshell> capitalize("what happens to -a +b 1c")
$15 ==> "What Happens To -A +B 1C"
Hint: Special Treatment Variant
刚才,我提出了另一个特例。如何处理它是一个定义问题。如果特殊字符后的字母不应转换为大写,这很容易实现。与之前相比,差别是微妙的:在每种情况下,您都删除了isLetter()检查并调用toUpperCase()。这是可能的,因为该方法不仅可以处理字母,还可以处理其他字符。
static String capitalize(final String input)
{
// ...
if (Character.isWhitespace(currentChar))
{
capitalizeNextChar = true;
}
else
{
if (capitalizeNextChar)
{
// convert to uppercase
inputChars[i] = Character.toUpperCase(currentChar);
capitalizeNextChar = false;
}
}
// ...
}
然后,您将获得以下输出:
jshell> capitalize("what happens to -a +b 1c")
$16 ==> "What Happens To -a +b 1c"
解决方案 7b:修改(★★✩✩✩)
现在假设输入是一个字符串列表,应该返回一个字符串列表,每个单词以大写字母开始。使用以下签名作为起点:
List<String> capitalize(List<String> words)
算法首先创建一个列表来存储转换后的单词。然后遍历所有传递的列表元素,并通过调用capitalizeWord()方法来处理每个元素。要将第一个字符转换成大写字母,请用substring(0, 1)分隔。其余字符由substring(1)返回。从两者中构建一个新单词,然后插入到结果中。为了容错,capitalizeWord()方法通过健全性检查来处理空输入,以避免在对substring()的后续调用中出现StringIndexOutOfBoundsException。
static List<String> capitalize(final List<String> words)
{
final List<String> capitalizedWords = new ArrayList<>();
for (final String word: words)
capitalizedWords.add(capitalizeWord(word));
return capitalizedWords;
}
static String capitalizeWord(final String word)
{
if (word.isEmpty())
return "";
final String upperCaseFirstChar = word.substring(0, 1).toUpperCase();
final String remainingChars = word.substring(1);
return upperCaseFirstChar + remainingChars;
}
您也可以使用流 API 来表达功能,而不是传统的for循环。有时这增加了源代码的可读性。为了实现这一点,我更喜欢管道布局,其中流方法一个接一个地编写,以反映流中的处理步骤:
static List<String> capitalizeWithStream(final List<String> words)
{
return words.stream().map(word -> capitalizeWord(word)).
collect(Collectors.toList());
}
解决方案 7c:特殊待遇(★★✩✩✩)
在标题中,经常会遇到像“is”或“a”这样的非大写单词的特殊处理。将它实现为一个方法List<String> capitalizeSpecial(List<String>, List<String>,该方法获取要从转换中排除的单词作为第二个参数。
例子
|投入
|
例外
|
结果
| | --- | --- | --- | | 【“这个”、“是”、“一个”、“标题”】 | [“是”,“一个”] | 【“这个”、“是”、“一个”、“标题”】 |
算法之前开发的功能,用一个不应该转换的单词列表来扩展。遍历时,检查当前单词是否是否定列表中的一个。如果是,则不加修改地添加到结果中。否则,您可以像以前一样执行操作。
static List<String> capitalizeSpecial(final List<String> words,
final List<String> ignorableWords)
{
final List<String> capitalizedWords = new ArrayList<>();
for (final String word : words)
{
if (word.length() > 0)
{
if (ignorableWords.contains(word))
capitalizedWords.add(word);
else
capitalizedWords.add(capitalizeWord(word));
}
}
return capitalizedWords;
}
确认
对于测试,您使用以下输入,这些输入显示了正确的操作:
@ParameterizedTest(name = "capitalize({0}) => {1}")
@CsvSource({ "this is a very special title, This Is A Very Special Title", "effective java is great, Effective Java Is Great" })
void capitalize(String input, String expected)
{
var result = Ex07_Capitalize.capitalize(input);
assertEquals(expected, result);
}
@Test
void capitalizeWithList()
{
List<String> input = List.of("this", "is", "a", "special", "title");
var result = Ex07_Capitalize.capitalize(input);
assertEquals(List.of("This", "Is", "A", "Special", "Title"), result);
}
@Test
void capitalizeSpecial()
{
List<String> input = List.of("this", "is", "a", "special", "title");
var result = Ex07_Capitalize.capitalizeSpecial(input, List.of("is", "a"));
assertEquals(List.of("This", "is", "a", "Special", "Title"), result);
}
4.3.8 解决方案 8:轮换(★★✩✩✩)
考虑两个字符串,str1和str2,其中第一个字符串应该比第二个长。弄清楚第一个是否包含另一个。这样做时,第一字符串内的字符也可以被旋转;字符可以从开头或结尾移动到相反的位置(甚至重复)。为此,创建方法boolean containsRotation(String, String),它在检查过程中不区分大小写。
例子
|输入 1
|
输入 2
|
结果
| | --- | --- | --- | | “ABCD” | “ABC” | 真实的 | | “ABCDEF | 《EFAB》 | 真(“abcdef”↢x 2“CD EFAB”包含“efab”) | | 《BCDE》 | "欧共体" | 错误的 | | “挑战” | "壁虎" | 真实的 |
Job Interview Tips: Possible Questions and Solution Ideas
作为求职面试的一个例子,在这里,我再次提到你可能会问一些问题来澄清任务:
-
旋转的方向是否已知 ← / → ?回答:不,任意
-
旋转检查应该区分大小写吗?回答:不,同等对待
**想法一:暴力:**作为第一个想法,你可以尝试所有的组合。不旋转启动。然后向左旋转弦str1并检查该旋转的弦是否包含在str2中。在最坏的情况下,这个过程重复 n 次。这是极其低效的。
**想法 2:首先检查旋转是否有意义:**解决这个问题的另一个想法是预先收集每个字符串中的所有字符,然后使用containsAll()检查是否包含所有需要的字母。但是即使这样也很费力,并且不能很好地反映要解决的问题。
想法三:现实中的程序:思考一会儿,考虑你可能如何在一张纸上解决问题。在某些时候,你会想把这个单词按顺序写两次:
ABCDEF EFAB
ABCDEFABCDEF EFAB
算法检查一个字符串是否可以出现在另一个字符串中(如果旋转的话),可以通过在另一个字符串后面写入更长的字符串的简单技巧非常优雅地解决。在组合中,检查要搜索的字符串是否包含在其中。使用这种方法,解决方案非常短而且非常简单:
static boolean containsRotation(final String str1, final String str2)
{
final String newDoubledStr1 = (str1 + str1).toLowerCase();
return newDoubledStr1.indexOf(str2.toLowerCase()) != -1;
}
确认
测试时,使用以下显示正确操作的输入:
@ParameterizedTest(name = "{1} in {0}{0} => {2}")
@CsvSource({ "ABCD, ABC, true", "ABCDEF, EFAB, true", "BCDE, EC, false",
"Challenge, GECH, true"})
void containsRotation(String value, String rotatedSub, boolean expected)
{
boolean result = Ex08_RotationV2.containsRotation(value, rotatedSub);
assertEquals(expected, result);
}
4.3.9 解决方案 9:格式良好的括号(★★✩✩✩)
编写方法boolean checkBraces(String),它检查作为字符串传递的圆括号序列是否包含匹配的(正确嵌套的)括号对。
例子
|投入
|
结果
|
评论
| | --- | --- | --- | | "(())" | 真实的 | | | "()()" | 真实的 | | | "(()))((())" | 错误的 | 虽然相同数量的左大括号和右大括号,但它们没有正确嵌套 | | "((()" | 错误的 | 没有合适的支撑 |
不经过太多考虑,人们可能会尝试所有可能的组合。经过一番思考,你大概得出了以下优化:你只统计左大括号的个数,并适当地与右大括号的个数进行比较。在开始大括号之前,您必须考虑结束大括号的细节。如下进行:从前到后遍历字符串。如果当前字符是左大括号,则将左大括号的计数器加 1。如果是右大括号,则将计数器减一。如果计数器低于 0,您将遇到一个没有相应的左大括号的右大括号。最后,计数器必须等于 0,以便它表示正确的支撑。
static boolean checkBraces(final String input)
{
int openingCount = 0;
for (int i = 0; i < input.length(); i++)
{
final char ch = input.charAt(i);
if (ch == '(')
{
openingCount++;
}
else if (ch == ')')
{
openingCount--;
if (openingCount < 0)
return false;
}
}
return openingCount == 0;
}
确认
使用参数化测试的以下输入来测试您新开发的检查是否正确——使用附加提示参数作为技巧,这不用于测试,而仅用于准备信息性 JUnit 输出:
@ParameterizedTest(name = "checkBraces(''{0}'') -- hint: {2}")
@CsvSource({ "(()), true, ok",
"()(), true, ok",
"(()))((()), false, not properly nested",
"((), false, no suitable bracing" })
void checkBraces(String input, boolean expected, String hint)
{
boolean result = Ex09_SimpleBracesChecker.checkBraces(input);
assertEquals(expected, result);
}
4.3.10 解决办法 10: Anagram
术语变位词用于描述两个包含相同频率的相同字母的字符串。在这里,大写和小写不应该有任何区别。写方法boolean isAnagram(String, String)。
例子
|输入 1
|
输入 2
|
结果
| | --- | --- | --- | | “奥托” | “托托” | 真实的 | | “玛丽 | “军队” | 真实的 | | “阿纳纳斯” | “香蕉” | 错误的 |
算法练习的描述已经为你如何进行提供了线索。首先,用方法calcCharFrequencies(String)将单词转换成直方图。在这里,您逐个字符地遍历相应的单词,并填充一个Map<K,V>。这是为两个单词做的。之后,您可以轻松地比较这两张地图:
static boolean isAnagram(final String str1, final String str2)
{
final Map<Character, Integer> charCounts1 = calcCharFrequencies(str1);
final Map<Character, Integer> charCounts2 = calcCharFrequencies(str2);
return charCounts1.equals(charCounts2);
}
static Map<Character, Integer> calcCharFrequencies(final String input)
{
final Map<Character, Integer> charCounts = new TreeMap<>();
for (char currentChar : input.toUpperCase().toCharArray())
{
charCounts.putIfAbsent(currentChar, 0);
charCounts.computeIfPresent(currentChar, (key, value) -> value + 1);
}
return charCounts;
}
确认
对于测试,使用以下显示正确功能的输入:
@ParameterizedTest(name = "isAnagram({0}, {1}) => {2}")
@CsvSource({ "Otto, Toto, true", "Mary, Army, true",
"Ananas, Bananas, false" })
void testIsAnagram(String value1, String value2, boolean expected)
{
boolean result = Ex10_AnagramChecker.isAnagram(value1, value2);
assertEquals(expected, result);
}
4.3.11 解决方案 11:莫尔斯电码(★★✩✩✩)
能够将给定文本翻译成莫尔斯电码字符的书写方法String toMorseCode(String)。它们由每个字母一至四个长短音调的序列组成,用句点(.)或连字符(-)。为了更容易区分,希望在每个音调之间放置一个空格,在每个字母音调序列之间放置三个空格。否则,S(...)和 EEE(...)将无法彼此区分。
为简单起见,将自己限制为字母 E、O、S、T 和 W,编码如下:
|信
|
莫尔斯电码
| | --- | --- | | E | 。 | | O | - - - | | S | ... | | T | - | | W | 。- - |
例子
|投入
|
结果
| | --- | --- | | 无线电紧急呼救信号 | 。。。- - - .。。 | | 自录音再现装置发出的高音 | - .- - .。- | | 西方的 | 。- - .。。。- |
算法一个字符一个字符地遍历字符串,当前字符被映射到相应的莫尔斯电码。方法convertToMorseCode(char)执行此任务:
static String toMorseCode(final String input)
{
final StringBuilder convertedMsg = new StringBuilder();
final String upperCaseInput = input.toUpperCase();
for (int i = 0; i < upperCaseInput.length(); i++)
{
var currentChar = upperCaseInput.charAt(i);
var convertedLetter = convertToMorseCode(currentChar);
convertedMsg.append(convertedLetter);
convertedMsg.append(" ");
}
return convertedMsg.toString().trim();
}
要映射单个字母,使用switch——Java 14 中新引入的语法比以前更加优雅地实现了这一点。 2
static String convertToMorseCode(char currentChar)
{
return switch (currentChar)
{
case 'E' -> ".";
case 'O' -> "- - -";
case 'S' -> ". . .";
case 'T' -> "-";
case 'W' -> ". - -";
default -> "?";
};
}
奖金
尝试找出字母表中所有字母对应的莫尔斯电码,(例如,转换你的名字)。你可以在 https://en.wikipedia.org/wiki/Morse_code 找到必要的提示。
算法您可以方便地用一个查找映射替换switch语句,然后只访问那个映射,而不是使用方法convertToMorseCode():
static Map<Character, String> lookupMap = new HashMap<>()
{{
put('A', ". -");
put('B', "- . . .");
put('C', "- . - .");
put('D', "- . .");
put('E', ".");
put('F', ". . - .");
put('G', "- - .");
put('H', ". . . .");
put('I', ". .");
// ..
put('R', ". - .");
put('O', "- - -");
put('S', ". . .");
put('T', "-");
put('W', ". - -");
// ...
}};
static String convertToMorseCode(char currentChar)
{
return lookupMap.getOrDefault(currentChar, "?");
}
为了进行实验,请使用互联网上的以下网站来验证您的尝试: https://gc.de/gc/morse/ 。它允许你把普通的明文转换成莫尔斯电码,但最重要的是,把莫尔斯电码转换成明文(反过来)。
确认
让我们使用单元测试进行检查,如下所示:
@ParameterizedTest(name = "toMorseCode({0}) => ''{1}''")
@CsvSource({ "SOS, . . . - - - . . .", "TWEET, - . - - . . -",
"OST, - - - . . . -", "WEST, . - - . . . . -" })
void testToMorseCode(String input, String expected)
{
var result = Ex11_MorseCode.toMorseCode(input);
assertEquals(expected, result);
}
4.3.12 解决方案 12:模式检查器(★★★✩✩)
编写方法boolean matchesPattern(String, String),该方法根据以单个字符的形式作为第一个参数传递的模式的结构来检查空格分隔的字符串(第二个参数)。
例子
|输入模式
|
输入文本
|
结果
| | --- | --- | --- | | " xyyx " | 《蒂姆·迈克·迈克·蒂姆》 | 真实的 | | " xyyx " | 《蒂姆·迈克·汤姆·蒂姆》 | 错误的 | | " xyxx " | 《蒂姆·迈克·迈克·蒂姆》 | 错误的 | | " xxxx " | "团队团队团队" | 真实的 |
Job Interview Tips: Problem Solving Strategies
像这样的练习,你应该总是问几个问题,以澄清背景,并获得更好的理解。对于此示例,可能的问题包括:
-
模式仅限于字符 x 和 y 吗?回答:没有,但每个字符只有一个字母作为占位符
-
模式总是只有四个字符长吗?回答:不,任意
-
图案不包含空格吗?回答:是的,从来没有
-
输入是否总是用一个空格分隔?回答:是的
算法和往常一样,首先理解问题并识别适当的数据结构是很重要的。您将模式规范识别为字符序列,将输入值识别为空格分隔的单词。这些可以使用split()转换成相应的单值数组。首先,检查模式的长度和输入值的数组是否匹配。只有在这种情况下,才能像以前多次做的那样,一个字符一个字符地遍历模式。作为一个辅助数据结构,您使用一个Map<K,V>将模式的单个字符映射到单词。现在检查是否已经为模式字符插入了另一个单词。通过使用这个技巧,您可以很容易地检测到映射错误。
static boolean matchesPattern(final String pattern, final String input)
{
// preparation
final int patternLength = pattern.length();
final String[] values = input.split(" ");
final int valuesLength = values.length;
if (valuesLength != patternLength ||
(values.length == 1 && values[0].isEmpty()))
return false;
final Map<Character, String> placeholderToValueMap = new HashMap<>();
// run through all characters of the pattern
for (int i = 0; i< pattern.length(); i++)
{
final char patternChar = pattern.charAt(i);
final String value = values[i];
// add, if not already there
placeholderToValueMap.putIfAbsent(patternChar, value);
// does stored value match current string?
final String assignedValue = placeholderToValueMap.get(patternChar);
if (!assignedValue.equals(value))
return false;
}
return true;
}
在代码中,在实际检查之前,您仍然需要显式验证空输入的特殊情况,因为" ".split(" ")会产生一个长度为 1 的数组。
该实现还允许以下规范,其中不同的通配符(以下称为y和z)被赋予相同的值(黑色):
matchesPattern("xyzx", "red black black red") => true
为了正确处理这种特殊情况,建议在第一次检查后执行以下查询:
// test for uniqueness of value
if (placeholderToValueMap.values().stream().
filter(str -> str.equals(value)).count() > 1)
return false;
确认
对于测试,您使用以下输入,这些输入显示了正确的操作:
@ParameterizedTest(name = "pattern ''{0}'' matches ''{1}'' => {2}")
@CsvSource( {"xyyx, tim mike mike tim, true",
"xyyx, time mike tom tim, false",
"xyxx, tim mike mike tim, false",
"xxxx, tim tim tim tim, true" })
void testInputMatchesPattern(String pattern, String input, boolean expected)
{
boolean result = Ex12_PatternChecker.matchesPattern(pattern, input);
assertEquals(expected, result);
}
4.3.13 解决方案 13:网球比分(★★★✩✩)
编写方法String tennisScore(String, String, String),根据两个玩家 PL1 和 PL2 的文本分数,以熟悉的风格发布公告,如十五爱、二或优势玩家 X 。因此,他们的分数以格式< PL1 分> : < PL2 分>给出。
以下计数规则适用于网球比赛:
-
当玩家达到 4 分或更多,并且领先至少 2 分时,游戏获胜(游戏)。
-
从 0 到 3 的分数被命名为爱,十五,三十和四十。
-
在至少 3 分和平局的情况下,这被称为平手。
-
至少有 3 分和 1 分的差距,对于多一分的人来说,这叫优势。
例子
|投入
|
得分
| | --- | --- | | 1:0 米夏蒂姆 | 《十五个爱》 | | 2:2 米夏蒂姆 | “三点半” | | 2:3 米夏蒂姆 | “三点四十” | | 3:3 米夏蒂姆 | “平手” | | 4:3 米夏蒂姆 | “优势米查” | | 4:4 米夏蒂姆 | “平手” | | 5:4 米夏蒂姆 | “优势米查” | | 6:4 米夏蒂姆 | 《游戏米莎》 |
算法在这种情况下,它是一个两步算法:
-
首先,应该从文本表示中获得两个
int值的分数。 -
然后,您的任务是根据这些值生成相应的文本分数名称。
在解析乐谱时,您可以依赖标准的 JDK 功能,比如String.split()和Integer.parseInt()。此外,对于可重用的功能,包含某些安全检查是合理的。首先,两个值都应该是正数。之后,分数上的细节将被测试:首先达到 4 分的玩家赢得比赛,但前提是他至少领先 2 分。如果两名球员都有 3 分或更多,那么分差必须小于 3 分。否则就不是网球中的有效状态。您将解析和检查提取到方法extractPoints(String)中。
private static int[] extractPoints(final String score)
{
final String[] values = score.trim().split(":");
if (values.length != 2)
throw new IllegalArgumentException("illegal format -- score has not
format <points>:<points>, e.g. 7:6");
final int score1 = Integer.parseInt(values[0]);
final int score2 = Integer.parseInt(values[1]);
// sanity check
if (score1 < 0 || score2 < 0)
throw new IllegalArgumentException("points must be > 0");
// verhindert sowohl z. B. 6:3 aber auch 5:1
if ((score1 > 4 || score2 > 4) && Math.abs(score1 - score2) > 2)
throw new IllegalArgumentException("point difference must be < 3, " +
"otherwise invalid score");
return new int[] { score1, score2 };
}
从输入中提取出用分号分隔的两个分数后,就可以继续进行转换了。同样,你使用一个多步骤决策程序。根据规则,一个简单的映射在分数低于 3 时起作用。这在地图上得到了完美的描述。从 3 分开始,可能会出现平局、优势或赢得比赛。如果一个玩家最多得 2 分,另一个玩家也有可能以 4 分获胜。对于获胜的消息,只需要确定两个玩家谁的点数多就可以了。所描述的逻辑实现如下:
static String calculateScore(final String score,
final String player1Name,
final String player2Name)
{
final int[] points = extractPoints(score);
final int score1 = points[0];
final int score2 = points[1];
if (score1 >= 3 && score2 >= 3)
{
return generateInfo(score1, score2, player1Name, player2Name);
}
else if (score1 >= 4 || score2 >= 4)
{
var playerName = (score1 > score2 ? player1Name : player2Name);
return "Game " + playerName;
}
else
{
// special naming
var pointNames = Map.of(0, "Love", 1, "Fifteen",
2, "Thirty", 3, "Forty");
return pointNames.get(score1) + " " + pointNames.get(score2);
}
}
只剩下最后一个细节,即优势或胜利提示文本的生成:
static String generateInfo(final int score1,
final int score2,
final String player1Name,
final String player2Name)
{
final int scoreDifference = Math.abs(score1 - score2);
final String playerName = (score1 > score2 ? player1Name : player2Name);
if (score1 == score2)
return "Deuce";
if (scoreDifference == 1)
return "Advantage " + playerName;
if (scoreDifference == 2)
return "Game " + playerName;
throw new IllegalStateException("Unexpected difference: " + scoreDifference);
}
确认
让我们用一个假想的游戏来测试网球得分功能:
@ParameterizedTest(name = "''{0}'' => ''{1}''")
@CsvSource({ "1:0, Fifteen Love", "2:2, Thirty Thirty", "2:3, Thirty Forty",
"3:3, Deuce", "4:3, Advantage Micha", "4:4, Deuce",
"5:4, Advantage Micha", "6:4, Game Micha" }
void calculateScore(String score, String expected)
{
String result = Ex13_TennisPoints.calculateScore(score, "Micha", "Tim");
assertEquals(expected, result);
}
图 4-2 显示了 Eclipse 中测试执行的输出。
图 4-2
在 Eclipse 中测试网球比分的执行
你应该添加更多想象中的游戏序列,以巧妙地涵盖势均力敌和无可争议的胜利的边缘情况:
@ParameterizedTest(name = "''{0}'' => ''{1}''")
@CsvSource({ "1:0, Fifteen Love", "2:2, Thirty Thirty",
"3:2, Forty Thirty", "4:2, Game Micha" })
void calculateScoreWin(String score, String expected)
{
String result = Ex13_TennisPoints.calculateScore(score, "Micha", "Tim");
assertEquals(expected, result);
}
@ParameterizedTest(name = "''{0}'' => ''{1}''")
@CsvSource({ "1:0, Fifteen Love", "2:0, Thirty Love",
"3:0, Forty Love", "4:0, Game Micha"} )
void calculateScoreStraightWin(String score, String expected)
{
String result = Ex13_TennisPoints.calculateScore(score, "Micha", "Tim");
assertEquals(expected, result);
}
4.3.14 解决方案 14:版本号(★★✩✩✩)
编写方法int compareVersions(String, String),允许你以主的格式比较版本号。小调。互相贴片——因此贴片的规格是可选的。特别是,返回值应该与来自Comparator<T>接口的int compare(T, T)方法兼容。
例子
|版本 1
|
版本 2
|
结果
| | --- | --- | --- | | 1.11.17 | 2.3.5 | < | | Two point one | 2.1.3 | < | | 2.3.5 | Two point four | < | | Three point one | Two point four | > | | Three point three | 3.2.9 | > | | 7.2.71 | 7.2.71 | = |
算法通过调用split()将文本版本号细分为一个String[]。迭代提取的组件,并使用Integer.valueOf()将它们转换成版本号。然后从大调开始与Integer.compare()成对比较,然后根据需要从小调、和补丁开始比较。如果一个输入的值比另一个多,则不使用最后一个数字,除非版本号与该组件匹配,例如 3.1 和 3.1.7:
static int compareVersions(final String v1, final String v2)
{
var v1Numbers = v1.split("\\."); // caution: Reg-Ex, therefore
var v2Numbers = v2.split("\\."); // would be '.' for each character
int pos = 0;
int compareResult = 0;
while (pos < v1Numbers.length &&
pos < v2Numbers.length && compareResult == 0)
{
final int currentV1 = Integer.valueOf(v1Numbers[pos]);
final int currentV2 = Integer.valueOf(v2Numbers[pos]);
compareResult = Integer.compare(currentV1, currentV2);
pos++;
}
if (compareResult == 0) // same beginning for example 3.1 and 3.1.7
return Integer.compare(v1Numbers.length, v2Numbers.length);
return compareResult;
}
Pitfall: Regular Expression in Split()
当您将版本号分割成单独的组件时,也许您最初只是通过指定一个句点(.)作为split()中的人物。然而,我担心这是不对的,因为规范要求正则表达式,而句点(.)代表任何字符。
确认
您将使用参数化测试的以下输入来测试版本号的比较——这里再次使用附加提示参数的技巧:
@ParameterizedTest(name = "''{0}'' {3} ''{1}''")
@CsvSource({ "1.11.17, 2.3.5, -1, <", "2.3.5, 2.4, -1, <",
"2.1, 2.1.3, -1, <", "3.1, 2.4, 1, >",
"3.3, 3.2.9, 1, >", "7.2.71, 7.2.71, 0, =" })
void compareVersions(String v1, String v2, int expected, String hint)
{
int result = Ex14_VersionNumberComparator.compareVersions(v1, v2);
assertEquals(expected, result);
}
让我们来看看 Eclipse 中测试执行的易于理解的输出,如图 4-3 所示。
图 4-3
Eclipse 中的测试执行
奖金
使用Comparator<T>接口实现功能。
算法比较功能可以通过以下一行程序几乎完全转换成比较器:
static Comparator<String> versioNumberComparator =
(v1, v2) -> compareVersions(v1, v2);
4.3.15 解决方案 15:转换 strToLong (★★✩✩✩)
将一个字符串转换成一个long。自己写方法long strToLong(String)。
Note
使用long.parseLong(value)可以轻松实现转换。不要显式使用它,而是自己实现整个转换。
例子
|投入
|
结果
|
| --- | --- |
| “+123” | One hundred and twenty-three |
| “-123” | -123 |
| “7271” | Seven thousand two hundred and seventy-one |
| “ABC” | IllegalArgumentException |
| “0123” | 83(对于奖励任务) |
| “-0123” | -83(对于奖励任务) |
| “0128” | IllegalArgumentException(奖励任务) |
算法检查第一个字符是否为+/-并相应地设置一个标志isNegative。然后遍历所有字符,将其转换为数字。前一个值每次乘以 10,最后得到对应的数值。
static long strToLongV1(final String number)
{
final boolean isNegative = number.charAt(0) == '-';
long value = 0;
int pos = startsWithSign(number) ? 1 : 0;
while (pos < number.length())
{
final int digitValue = number.charAt(pos) - '0';
value = value * 10 + digitValue;
pos++;
}
return isNegative ? -value : value;
}
static boolean startsWithSign(final String number)
{
return number.charAt(0) == '-' || number.charAt(0) == '+';
}
修正算法即使不进行更深入的分析,也很明显上面的版本在混合字母和数字时无法正常工作。在这种情况下,通过使用isDigit()进行检查来抛出IllegalArgumentException是合理的,如下所示:
static long strToLongV2(final String number)
{
final boolean isNegative = number.charAt(0) == '-';
long value = 0;
int pos = startsWithSign(number) ? 1 : 0;
while (pos < number.length())
{
if (!Character.isDigit(number.charAt(pos)))
throw new IllegalArgumentException(number +
" contains not only digits");
final int digitValue = number.charAt(pos) - '0';
value = value * 10 + digitValue;
pos++;
}
return isNegative ? -value : value;
}
确认
要测试功能,您需要使用三个数字,带一个正号和一个负号,不带。在转换过程中,可以忽略正号。您分别检查输入字母而不是数字的反应,并期待出现异常。
@ParameterizedTest(name = "strToLongV2(\"{0}\") => {1}")
@CsvSource({ "+123, 123", "-123, -123", "123, 123", "7271, 7271" })
void testStrToLongV2(String number, long expected)
{
long result = Ex15_StrToLong.strToLongV2(number);
assertEquals(expected, result);
}
@Test
void testStrToLongV2Error()
{
assertThrows(IllegalArgumentException.class,
() -> Ex15_StrToLong.strToLongV2("ABC"));
}
额外收获:支持八进制数的解析
在 Java 中,八进制数由前导零标记。顾名思义,它们的基数是 8,而不是 10。为了支持八进制数,首先需要确定前导零是否存在。在这种情况下,数字系统中的位置系数更改为 8。最后,以 8 为基数,当然不再允许 8 和 9 这两个数字。因此,您在循环中添加了另一个检查来处理这些值。总而言之,由于特殊处理,源代码有点臃肿——复杂性只是可管理的——特别是因为这里使用了带有说话名称的问题适应帮助器方法。
static long strToLongBonus(final String number)
{
final boolean isNegative = number.charAt(0) == '-';
final boolean isOctal = number.charAt(0) == '0' ||
(startsWithSign(number) && number.charAt(1) == '0');
long value = 0;
final int factor = isOctal ? 8 : 10;
int pos = calcStartPos(number, isOctal);
while (pos < number.length())
{
if (!Character.isDigit(number.charAt(pos)))
throw new IllegalArgumentException(number + " contains not only
digits");
final int digitValue = number.charAt(pos) - '0';
if (isOctal && digitValue >= 8)
throw new IllegalArgumentException(number + " found digit >= 8");
value = value * factor + digitValue;
pos++;
}
return isNegative ? -value : value;
}
private static int calcStartPos(final String number, final boolean isOctal)
{
int pos = 0;
if (startsWithSign(number) && isOctal)
{
pos = 2;
}
else if (startsWithSign(number) || isOctal)
{
pos = 1;
}
return pos;
}
确认
要测试功能,请使用三个数字,带一个正号和一个负号,不带。在转换过程中,可以忽略正号。此外,检查一个正负八进制数。在单独的测试中,确保大于或等于 8 的数字不能出现在八进制数中。
@ParameterizedTest(name = "strToLongBonus(\"{0}\") => {1}")
@CsvSource({ "+123, 123", "-123, -123", "123, 123", "7271, 7271",
"+077, 63", "-077, -63", "077, 63",
"+0123, 83", "-0123, -83", "0123, 83" })
void testStrToLongBonus(String number, long expected)
{
long result = Ex15_StrToLong.strToLongBonus(number);
assertEquals(expected, result);
}
@Test
void strToLongBonus_should_raise_exception_for_invalid_octal_number()
{
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> Ex15_StrToLong.strToLongBonus("0128"));
assertTrue(ex.getMessage().contains("found digit >= 8"));
}
4.3.16 解决方案 16:印刷塔(★★★✩✩)
编写方法void printTower(int),将堆叠在一起的 n 片的塔表示为 ASCII 图形,用字符#表示,并绘制一条下边界线。
示例高度为 3 的塔应该是这样的:
|
#|#
##|##
###|###
---------
算法你可以把画图分为三步:画顶条,画切片,然后画底界。因此,可以使用三个方法调用来描述该算法:
static void printTower(final int height)
{
drawTop(height);
drawSlices(height);
drawBottom(height);
}
如前所述,您可以用两种辅助方法来绘制这座塔的各个组件:
static void drawTop(final int height)
{
System.out.println(repeatCharSequence(" ", height + 1) + "|");
}
static void drawBottom(final int height)
{
System.out.println(repeatCharSequence("-", (height + 1) * 2 + 1));
}
特别是这里使用了 helper 方法repeatCharSequence(),重复输出字符。
绘制塔的切片有点复杂,因为它们的大小不同,并且需要计算左右两侧的自由空间:
static void drawSlices(final int height)
{
for (int i = height - 1; i >= 0; i--)
{
final int value = height - i;
final int padding = i + 1;
final String line = repeatCharSequence(" ", padding) +
repeatCharSequence("#", value) +
"|" +
repeatCharSequence("#", value);
System.out.println(line);
}
}
static String repeatCharSequence(final String character, final int length)
{
String str = "";
for (int i = 0; i < length; i++)
{
str += character;
}
return str;
}
很明显,这个问题可以分解成越来越小的子问题。因此,每个方法都变得简短,并且通常也是可测试的(如果没有控制台输出,但是有返回的计算发生)。
对于 Java 11,不要使用上面的repeatCharSequence()方法,建议使用 JDK 的String.repeat()方法。为了尽可能少地改变,建议采用以下步骤。首先,不要调用自己的实现,只需调用repeatCharSequence()中的repeat()方法,如下所示:
static String repeatCharSequence(final String character, final int length)
{
return character.repeat(length);
}
为了清理不必要的委托,内联重构是有帮助的。它的使用移除了方法repeatCharSequence(),因此repeat()现在在所有地方都被直接调用。
确认
为了检查功能,再次使用 JShell 这里打印一个高度为 4 的塔:
jshell> printTower(4)
|
#|#
##|##
###|###
####|####
-----------
Hint: Modification with Recursion
有趣的是,塔的单个切片的绘制也可以递归地表达如下:
private static void drawSlices(final int slice, final int height)
{
if (slice > 1)
{
drawSlices(slice - 1, height);
System.out.println(repeatCharSequence(" ", height - slice + 1) +
repeatCharSequence("#", slice) +
"|" +
repeatCharSequence("#", slice));
}
}
然后,必须对调用进行最小程度的修改:
static void printTower(final int height)
{
drawTop(height);
drawSlices(height, height);
drawBottom(height);
}
Footnotes 1
关于更详细的治疗,我建议你参考我的书Der Weg zum Java ProFi【Ind20a】。
2
更多详细信息可在我的书 Java 中找到–版本 9 到 14 中的新增功能:模块化、语法和 API 扩展 [Ind20b]。