7. Спроектировать и реализовать сервис сокращения URL
Условие задачи:
Необходимо спроектировать и реализовать простой сервис сокращения URL.
Сервис должен уметь:
принимать длинный URL и возвращать короткий;
сохранять соответствие между коротким идентификатором и исходным URL;
по короткому URL возвращать исходный;
гарантировать уникальность создаваемых коротких идентификаторов.
Для учебной реализации данные можно хранить в памяти.
Спойлеры к решению
Подсказки
shortCode → originalUrl можно использовать Map.💡 Случайная генерация кода требует отдельно обрабатывать коллизии.
💡 Более простой вариант — генерировать уникальный числовой ID и кодировать его в Base62.
💡 Если один и тот же URL сокращается несколько раз, можно возвращать уже существующий короткий URL.
💡 Для многопоточного использования обычного
HashMap недостаточно.Решение
Определим интерфейс сервиса:
public interface UrlShortenerService {
String shorten(String originalUrl);
Optional<String> resolve(String shortUrl);
}
Простая потокобезопасная реализация в памяти:
public class InMemoryUrlShortenerService implements UrlShortenerService {
private static final String BASE_URL = "https://short.ly/";
private static final char[] BASE62 =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
.toCharArray();
private final AtomicLong sequence = new AtomicLong();
private final Map<String, String> codeToUrl =
new ConcurrentHashMap<>();
private final Map<String, String> urlToCode =
new ConcurrentHashMap<>();
@Override
public String shorten(String originalUrl) {
Objects.requireNonNull(originalUrl);
String code = urlToCode.computeIfAbsent(
originalUrl,
url -> {
String newCode = encode(sequence.incrementAndGet());
codeToUrl.put(newCode, url);
return newCode;
}
);
return BASE_URL + code;
}
@Override
public Optional<String> resolve(String shortUrl) {
Objects.requireNonNull(shortUrl);
if (!shortUrl.startsWith(BASE_URL)) {
return Optional.empty();
}
String code = shortUrl.substring(BASE_URL.length());
return Optional.ofNullable(codeToUrl.get(code));
}
private String encode(long value) {
StringBuilder result = new StringBuilder();
while (value > 0) {
int remainder = (int) (value % BASE62.length);
result.append(BASE62[remainder]);
value /= BASE62.length;
}
return result.reverse().toString();
}
}
Пример использования:
public static void main(String[] args) {
UrlShortenerService service =
new InMemoryUrlShortenerService();
String shortUrl = service.shorten(
"https://example.com/some/very/long/url"
);
System.out.println(shortUrl);
Optional<String> originalUrl =
service.resolve(shortUrl);
System.out.println(originalUrl.orElse("URL not found"));
}
Основное хранилище:
Map<String, String> codeToUrl
хранит соответствие:
короткий код → исходный URL
Обратная карта:
Map<String, String> urlToCode
позволяет при повторном сокращении того же URL вернуть уже существующий код.
Для создания короткого идентификатора используется последовательность:
sequence.incrementAndGet()
и преобразование числа в Base62:
1 → 1
61 → Z
62 → 10
63 → 11
...
Такой подход не требует случайной генерации и проверки коллизий: каждое новое значение счётчика уникально.
Для хранения используются ConcurrentHashMap, а для генерации ID — AtomicLong, поэтому базовая реализация подходит для вызовов из нескольких потоков.
В реальном сервисе вместо хранения данных в памяти понадобилось бы постоянное хранилище с уникальным ограничением на короткий код. Также обычно добавляют срок жизни ссылок, статистику переходов и распределённую генерацию идентификаторов.