在涉及支付的桌面客户端里,信用卡号属于敏感信息。直接把 16 位完整卡号一直显示在界面上,既不安全,也不美观。业界常见的做法是:用户点击输入框准备编辑时,显示完整卡号方便核对;一旦焦点离开输入框,就自动把内容变成类似 **** **** **** 1234 的掩码形式,只露出后四位。本文将详细讲解如何用 Java Swing 实现这样一个输入框,并顺带处理好输入过滤、格式化显示和失焦还原等细节。

一、核心思路:用 FocusListener 切换显示状态
实现这个效果的关键在于 Swing 提供的焦点事件机制。FocusListener 接口包含两个方法:focusGained 在组件获得焦点时触发,focusLost 在组件失去焦点时触发。我们可以维护一个真实卡号变量,获得焦点时把它写入文本框,失去焦点时把文本框内容替换为掩码字符串,同时判断是否需要清除已输入的值。
这里有一个容易被忽略的陷阱:如果把掩码文本直接写回文本框,那么下次获得焦点时,你拿到的将是掩码文本而不是真实卡号。因此必须把真实卡号单独保存在一个成员变量中,文本框只负责展示。下面是一个最基础的实现:
import javax.swing.*;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
public class CardNumberField extends JFrame {
// 保存真实的卡号,文本框只负责展示
private String realCardNumber = "";
private final JTextField cardField = new JTextField(20);
public CardNumberField() {
setTitle("信用卡号输入示例");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new FlowLayout());
cardField.setText("请输入卡号");
cardField.setHorizontalAlignment(JTextField.CENTER);
cardField.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
// 聚焦时显示真实卡号
cardField.setText(realCardNumber);
cardField.setCaretPosition(cardField.getText().length());
}
@Override
public void focusLost(FocusEvent e) {
// 失焦时保存真实值并显示掩码
String input = cardField.getText().trim();
if (input.matches("\\d{12,19}")) {
realCardNumber = input;
cardField.setText(maskCardNumber(realCardNumber));
} else {
realCardNumber = "";
cardField.setText("");
}
}
});
add(new JLabel("信用卡号:"));
add(cardField);
setSize(400, 120);
setLocationRelativeTo(null);
}
// 生成 **** **** **** 1234 形式的掩码
private String maskCardNumber(String card) {
String prefix = "*".repeat(card.length() - 4);
return prefix + card.substring(card.length() - 4);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new CardNumberField().setVisible(true));
}
}运行这段代码后,点击输入框可以正常输入数字,点击其他地方让输入框失焦,内容就会变成一串星号加上后四位。再次聚焦时又能看到完整卡号。这个基础版本已经能满足核心需求,但离生产可用还有距离,下面继续完善。
二、限制输入:只允许数字并自动分组
真实的信用卡号输入框不应该接受字母和符号。Swing 中控制输入内容的正规做法是自定义 DocumentFilter,它会在文档内容发生改变之前拦截插入和删除操作。相比在 keyTyped 事件里过滤,DocumentFilter 能同时拦截键盘输入和程序化的 setText 调用,可靠性更高。
另外一个体验优化是自动分组。信用卡号通常每四位一组显示,例如 4111 1111 1111 1111。分组只是视觉呈现,内部存储仍应保存纯数字。下面的过滤器会在用户输入时动态插入空格:
import javax.swing.text.*;
public class CardNumberDocumentFilter extends DocumentFilter {
private static final int MAX_DIGITS = 19; // 最长支持 19 位
@Override
public void insertString(FilterBypass fb, int offset, String text,
AttributeSet attrs) throws BadLocationException {
replace(fb, offset, 0, text, attrs);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text,
AttributeSet attrs) throws BadLocationException {
// 只保留数字
StringBuilder digits = new StringBuilder();
for (char c : text.toCharArray()) {
if (Character.isDigit(c)) {
digits.append(c);
}
}
if (digits.length() == 0 && length == 0) {
return;
}
// 取出替换前的完整文本,重新计算格式化结果
String current = fb.getDocument().getText(0, fb.getDocument().getLength());
String before = current.substring(0, offset);
String after = current.substring(offset + length);
String rawDigits = (before + digits + after).replaceAll("\\s", "");
if (rawDigits.length() > MAX_DIGITS) {
return; // 超出长度直接拒绝
}
String formatted = groupByFour(rawDigits);
fb.replace(0, fb.getDocument().getLength(), formatted, attrs);
}
// 每 4 位插入一个空格
private String groupByFour(String digits) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < digits.length(); i++) {
if (i > 0 && i % 4 == 0) {
sb.append(' ');
}
sb.append(digits.charAt(i));
}
return sb.toString();
}
}挂载方式很简单,拿到文本框的 PlainDocument 并调用 setDocumentFilter 即可:
AbstractDocument doc = (AbstractDocument) cardField.getDocument(); doc.setDocumentFilter(new CardNumberDocumentFilter());
需要注意的是,失焦时生成的掩码文本包含空格和星号,如果掩码文本也是通过带过滤器的路径写入的,就会被过滤器拒绝。解决办法是在写入掩码前判断当前是否处于失焦处理流程,用一个布尔标志临时绕过过滤器,或者直接操作 realCardNumber 变量,聚焦时再写入格式化后的数字。后者实现更简单,也是推荐做法。
三、细节打磨:光标、复制与卡号校验
第一是光标位置。聚焦时如果直接 setText,光标默认停留在开头,用户续输会很别扭。上面的代码已经用 setCaretPosition 把光标移到末尾。更好的体验是记住失焦前的光标位置,聚焦时恢复到原来的地方,特别是用户在中途修改某一位数字时。
第二是复制行为。出于安全考虑,很多支付产品禁止从卡号框复制完整卡号。如果希望复制的也是掩码文本,无需额外处理;如果希望禁止复制,可以重写 copy 方法或者使用 JPasswordField。JPasswordField 的 getText 已被标记为不推荐,应改用 getPassword 返回字符数组,用完及时清空,避免敏感信息长期驻留内存。
第三是卡号合法性校验。可以在失焦时顺带做一次 Luhn 校验,它几乎被所有主流卡组织采用,实现只需要几行代码:
// Luhn 算法校验卡号
private boolean isValidCardNumber(String digits) {
int sum = 0;
boolean alternate = false;
for (int i = digits.length() - 1; i >= 0; i--) {
int n = digits.charAt(i) - '0';
if (alternate) {
n *= 2;
if (n > 9) {
n -= 9;
}
}
sum += n;
alternate = !alternate;
}
return sum % 10 == 0;
}校验失败时可以在输入框旁边给出红色提示,或者直接把边框染成红色:cardField.setBorder(BorderFactory.createLineBorder(Color.RED)),校验通过后再恢复默认边框。需要注意的是,失焦校验和掩码显示应该同时进行:即使卡号校验不通过,掩码逻辑也不应中断,否则用户每次失焦都会看到完整卡号,保护效果就失效了。
最后补充一点架构层面的建议。真实卡号的持有者最好不要是输入框组件本身,而是引入一个简单的模型类(例如 CardModel),输入框只作为视图监听模型变化。这样后续如果要把卡号提交给后端、记录日志或做加密存储,都通过模型操作,界面层不接触明文逻辑,代码职责更清晰,也更容易写单元测试。
总结一下,实现聚焦显示全部、失焦只显示后四位的信用卡输入框,核心是 FocusListener 配合独立保存的真实卡号变量,再叠加 DocumentFilter 做输入过滤和四位分组,最后用 Luhn 校验和光标管理提升体验。把这些组件组合起来,就能得到一个安全、规范且好用的 Swing 卡号录入控件。
Java Swing JFormattedTextField FocusListener修改时间:2026-08-31 21:39:11